create-gen-app 0.3.5 → 0.4.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 +5 -2
- package/esm/index.js +15 -15
- package/esm/licenses.js +22 -22
- package/esm/template/extract.js +30 -24
- package/esm/template/prompt.js +6 -6
- package/esm/template/replace.js +6 -3
- package/index.d.ts +16 -16
- package/licenses.js +20 -20
- package/package.json +5 -5
- package/template/extract.d.ts +1 -1
- package/template/extract.js +28 -22
- package/template/prompt.d.ts +2 -2
- package/template/prompt.js +5 -5
- package/template/replace.js +6 -3
- package/types.d.ts +1 -1
package/README.md
CHANGED
|
@@ -114,10 +114,11 @@ export const author = "____fullName____";
|
|
|
114
114
|
|
|
115
115
|
### Custom Questions
|
|
116
116
|
|
|
117
|
-
Create a `.
|
|
117
|
+
Create a `.boilerplate.json`:
|
|
118
118
|
|
|
119
119
|
```json
|
|
120
120
|
{
|
|
121
|
+
"type": "module",
|
|
121
122
|
"questions": [
|
|
122
123
|
{
|
|
123
124
|
"name": "____fullName____",
|
|
@@ -135,7 +136,9 @@ Create a `.questions.json`:
|
|
|
135
136
|
}
|
|
136
137
|
```
|
|
137
138
|
|
|
138
|
-
Or `.
|
|
139
|
+
Or `.boilerplate.js` for dynamic logic. Question names can use `____var____` or plain `VAR`; they'll be normalized automatically.
|
|
140
|
+
|
|
141
|
+
Note: `.boilerplate.json`, `.boilerplate.js`, and `.boilerplates.json` files are automatically excluded from the generated output.
|
|
139
142
|
|
|
140
143
|
### License Templates
|
|
141
144
|
|
package/esm/index.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { extractVariables } from
|
|
2
|
-
import { promptUser } from
|
|
3
|
-
import { replaceVariables } from
|
|
1
|
+
import { extractVariables } from './template/extract';
|
|
2
|
+
import { promptUser } from './template/prompt';
|
|
3
|
+
import { replaceVariables } from './template/replace';
|
|
4
4
|
// Export new modular classes
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
12
|
-
export * from
|
|
5
|
+
export * from './cache/cache-manager';
|
|
6
|
+
export * from './cache/types';
|
|
7
|
+
export * from './git/git-cloner';
|
|
8
|
+
export * from './git/types';
|
|
9
|
+
export * from './template/templatizer';
|
|
10
|
+
export * from './template/types';
|
|
11
|
+
export * from './utils/npm-version-check';
|
|
12
|
+
export * from './utils/types';
|
|
13
13
|
// Export template processing functions
|
|
14
|
-
export * from
|
|
15
|
-
export * from
|
|
16
|
-
export * from
|
|
14
|
+
export * from './template/extract';
|
|
15
|
+
export * from './template/prompt';
|
|
16
|
+
export * from './template/replace';
|
|
17
17
|
// Export shared types
|
|
18
|
-
export * from
|
|
18
|
+
export * from './types';
|
|
19
19
|
// DEPRECATED: Legacy exports for backward compatibility (will be removed in future)
|
|
20
20
|
// Use CacheManager, GitCloner, and Templatizer classes instead
|
|
21
21
|
export { extractVariables, promptUser, replaceVariables };
|
package/esm/licenses.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
3
|
const PLACEHOLDER_PATTERN = /{{(\w+)}}/g;
|
|
4
4
|
let cachedTemplates = null;
|
|
5
|
-
export const LICENSE_VALUE_KEYS = [
|
|
5
|
+
export const LICENSE_VALUE_KEYS = ['LICENSE', 'license'];
|
|
6
6
|
export const LICENSE_AUTHOR_KEYS = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
'USERFULLNAME',
|
|
8
|
+
'AUTHOR',
|
|
9
|
+
'AUTHORFULLNAME',
|
|
10
|
+
'USERNAME',
|
|
11
|
+
'fullName',
|
|
12
|
+
'author',
|
|
13
|
+
'authorFullName',
|
|
14
|
+
'userName',
|
|
15
15
|
];
|
|
16
|
-
export const LICENSE_EMAIL_KEYS = [
|
|
16
|
+
export const LICENSE_EMAIL_KEYS = ['USEREMAIL', 'EMAIL', 'email', 'userEmail'];
|
|
17
17
|
export function isSupportedLicense(name) {
|
|
18
18
|
if (!name) {
|
|
19
19
|
return false;
|
|
@@ -31,18 +31,18 @@ export function renderLicense(licenseName, context) {
|
|
|
31
31
|
}
|
|
32
32
|
const ctx = {
|
|
33
33
|
year: context.year ?? new Date().getFullYear().toString(),
|
|
34
|
-
author: context.author ??
|
|
35
|
-
email: context.email ??
|
|
34
|
+
author: context.author ?? 'Unknown Author',
|
|
35
|
+
email: context.email ?? '',
|
|
36
36
|
};
|
|
37
|
-
const emailLine = ctx.email ? ` <${ctx.email}>` :
|
|
37
|
+
const emailLine = ctx.email ? ` <${ctx.email}>` : '';
|
|
38
38
|
return template.replace(PLACEHOLDER_PATTERN, (_, rawKey) => {
|
|
39
39
|
const key = rawKey.toUpperCase();
|
|
40
|
-
if (key ===
|
|
40
|
+
if (key === 'EMAIL_LINE') {
|
|
41
41
|
return emailLine;
|
|
42
42
|
}
|
|
43
43
|
const normalizedKey = key.toLowerCase();
|
|
44
44
|
const value = ctx[normalizedKey];
|
|
45
|
-
return value ||
|
|
45
|
+
return value || '';
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
export function listSupportedLicenses() {
|
|
@@ -74,19 +74,19 @@ function loadLicenseTemplates() {
|
|
|
74
74
|
if (!stats.isFile()) {
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
|
-
if (path.extname(file).toLowerCase() !==
|
|
77
|
+
if (path.extname(file).toLowerCase() !== '.txt') {
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
const key = path.basename(file, path.extname(file)).toUpperCase();
|
|
81
|
-
templates[key] = fs.readFileSync(fullPath,
|
|
81
|
+
templates[key] = fs.readFileSync(fullPath, 'utf8');
|
|
82
82
|
}
|
|
83
83
|
cachedTemplates = templates;
|
|
84
84
|
return cachedTemplates;
|
|
85
85
|
}
|
|
86
86
|
function findTemplatesDir() {
|
|
87
87
|
const candidates = [
|
|
88
|
-
path.resolve(__dirname,
|
|
89
|
-
path.resolve(__dirname,
|
|
88
|
+
path.resolve(__dirname, '..', 'licenses-templates'),
|
|
89
|
+
path.resolve(__dirname, 'licenses-templates'),
|
|
90
90
|
];
|
|
91
91
|
for (const candidate of candidates) {
|
|
92
92
|
if (fs.existsSync(candidate)) {
|
|
@@ -98,7 +98,7 @@ function findTemplatesDir() {
|
|
|
98
98
|
function getAnswerValue(answers, keys) {
|
|
99
99
|
for (const key of keys) {
|
|
100
100
|
const value = answers?.[key];
|
|
101
|
-
if (typeof value ===
|
|
101
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
102
102
|
return value;
|
|
103
103
|
}
|
|
104
104
|
}
|
package/esm/template/extract.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as fs from
|
|
2
|
-
import * as path from
|
|
3
|
-
const PLACEHOLDER_BOUNDARY =
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
const PLACEHOLDER_BOUNDARY = '____';
|
|
4
4
|
/**
|
|
5
5
|
* Pattern to match ____variable____ in filenames and content
|
|
6
6
|
*/
|
|
7
|
-
const VARIABLE_PATTERN = new RegExp(`${PLACEHOLDER_BOUNDARY}([A-Za-z_][A-Za-z0-9_]*)${PLACEHOLDER_BOUNDARY}`,
|
|
7
|
+
const VARIABLE_PATTERN = new RegExp(`${PLACEHOLDER_BOUNDARY}([A-Za-z_][A-Za-z0-9_]*)${PLACEHOLDER_BOUNDARY}`, 'g');
|
|
8
8
|
/**
|
|
9
9
|
* Extract all variables from a template directory
|
|
10
10
|
* @param templateDir - Path to the template directory
|
|
@@ -18,8 +18,10 @@ export async function extractVariables(templateDir) {
|
|
|
18
18
|
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
19
19
|
await walkDirectory(templateDir, async (filePath) => {
|
|
20
20
|
const relativePath = path.relative(templateDir, filePath);
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Skip boilerplate configuration files from variable extraction
|
|
22
|
+
if (relativePath === '.boilerplate.json' ||
|
|
23
|
+
relativePath === '.boilerplate.js' ||
|
|
24
|
+
relativePath === '.boilerplates.json') {
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
27
|
const matches = relativePath.matchAll(VARIABLE_PATTERN);
|
|
@@ -29,7 +31,7 @@ export async function extractVariables(templateDir) {
|
|
|
29
31
|
fileReplacerVars.add(varName);
|
|
30
32
|
fileReplacers.push({
|
|
31
33
|
variable: varName,
|
|
32
|
-
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
|
|
34
|
+
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`, 'g'),
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -39,7 +41,7 @@ export async function extractVariables(templateDir) {
|
|
|
39
41
|
contentReplacerVars.add(varName);
|
|
40
42
|
contentReplacers.push({
|
|
41
43
|
variable: varName,
|
|
42
|
-
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
|
|
44
|
+
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`, 'g'),
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -58,12 +60,12 @@ export async function extractVariables(templateDir) {
|
|
|
58
60
|
async function extractFromFileContent(filePath) {
|
|
59
61
|
const variables = new Set();
|
|
60
62
|
return new Promise((resolve) => {
|
|
61
|
-
const stream = fs.createReadStream(filePath, { encoding:
|
|
62
|
-
let buffer =
|
|
63
|
-
stream.on(
|
|
63
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
64
|
+
let buffer = '';
|
|
65
|
+
stream.on('data', (chunk) => {
|
|
64
66
|
buffer += chunk.toString();
|
|
65
|
-
const lines = buffer.split(
|
|
66
|
-
buffer = lines.pop() ||
|
|
67
|
+
const lines = buffer.split('\n');
|
|
68
|
+
buffer = lines.pop() || ''; // Keep the last incomplete line in buffer
|
|
67
69
|
for (const line of lines) {
|
|
68
70
|
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
69
71
|
for (const match of matches) {
|
|
@@ -71,14 +73,14 @@ async function extractFromFileContent(filePath) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
});
|
|
74
|
-
stream.on(
|
|
76
|
+
stream.on('end', () => {
|
|
75
77
|
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
76
78
|
for (const match of matches) {
|
|
77
79
|
variables.add(match[1]);
|
|
78
80
|
}
|
|
79
81
|
resolve(variables);
|
|
80
82
|
});
|
|
81
|
-
stream.on(
|
|
83
|
+
stream.on('error', () => {
|
|
82
84
|
resolve(variables);
|
|
83
85
|
});
|
|
84
86
|
});
|
|
@@ -101,33 +103,37 @@ async function walkDirectory(dir, callback) {
|
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
/**
|
|
104
|
-
* Load project questions from .
|
|
106
|
+
* Load project questions from .boilerplate.json or .boilerplate.js
|
|
105
107
|
* @param templateDir - Path to the template directory
|
|
106
108
|
* @returns Questions object or null if not found
|
|
107
109
|
*/
|
|
108
110
|
async function loadProjectQuestions(templateDir) {
|
|
109
|
-
const jsonPath = path.join(templateDir,
|
|
111
|
+
const jsonPath = path.join(templateDir, '.boilerplate.json');
|
|
110
112
|
if (fs.existsSync(jsonPath)) {
|
|
111
113
|
try {
|
|
112
|
-
const content = fs.readFileSync(jsonPath,
|
|
113
|
-
const
|
|
114
|
+
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
115
|
+
const parsed = JSON.parse(content);
|
|
116
|
+
// .boilerplate.json has { questions: [...] } structure
|
|
117
|
+
const questions = parsed.questions ? { questions: parsed.questions } : parsed;
|
|
114
118
|
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
115
119
|
}
|
|
116
120
|
catch (error) {
|
|
117
121
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
118
|
-
console.warn(`Failed to parse .
|
|
122
|
+
console.warn(`Failed to parse .boilerplate.json: ${errorMessage}`);
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
|
-
const jsPath = path.join(templateDir,
|
|
125
|
+
const jsPath = path.join(templateDir, '.boilerplate.js');
|
|
122
126
|
if (fs.existsSync(jsPath)) {
|
|
123
127
|
try {
|
|
124
128
|
const module = require(jsPath);
|
|
125
|
-
const
|
|
129
|
+
const exported = module.default || module;
|
|
130
|
+
// .boilerplate.js can export { questions: [...] } or just the questions array
|
|
131
|
+
const questions = exported.questions ? { questions: exported.questions } : exported;
|
|
126
132
|
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
127
133
|
}
|
|
128
134
|
catch (error) {
|
|
129
135
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
130
|
-
console.warn(`Failed to load .
|
|
136
|
+
console.warn(`Failed to load .boilerplate.js: ${errorMessage}`);
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
return null;
|
|
@@ -152,5 +158,5 @@ function normalizeQuestions(questions) {
|
|
|
152
158
|
* @returns True if valid, false otherwise
|
|
153
159
|
*/
|
|
154
160
|
function validateQuestions(obj) {
|
|
155
|
-
return obj && typeof obj ===
|
|
161
|
+
return obj && typeof obj === 'object' && Array.isArray(obj.questions);
|
|
156
162
|
}
|
package/esm/template/prompt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Inquirerer } from
|
|
2
|
-
const PLACEHOLDER_BOUNDARY =
|
|
1
|
+
import { Inquirerer } from 'inquirerer';
|
|
2
|
+
const PLACEHOLDER_BOUNDARY = '____';
|
|
3
3
|
/**
|
|
4
4
|
* Generate questions from extracted variables
|
|
5
5
|
* @param extractedVariables - Variables extracted from the template
|
|
@@ -20,7 +20,7 @@ export function generateQuestions(extractedVariables) {
|
|
|
20
20
|
if (!askedVariables.has(replacer.variable)) {
|
|
21
21
|
questions.push({
|
|
22
22
|
name: replacer.variable,
|
|
23
|
-
type:
|
|
23
|
+
type: 'text',
|
|
24
24
|
message: `Enter value for ${replacer.variable}:`,
|
|
25
25
|
required: true
|
|
26
26
|
});
|
|
@@ -31,7 +31,7 @@ export function generateQuestions(extractedVariables) {
|
|
|
31
31
|
if (!askedVariables.has(replacer.variable)) {
|
|
32
32
|
questions.push({
|
|
33
33
|
name: replacer.variable,
|
|
34
|
-
type:
|
|
34
|
+
type: 'text',
|
|
35
35
|
message: `Enter value for ${replacer.variable}:`,
|
|
36
36
|
required: true
|
|
37
37
|
});
|
|
@@ -45,7 +45,7 @@ function normalizeQuestionName(name) {
|
|
|
45
45
|
name.endsWith(PLACEHOLDER_BOUNDARY)) {
|
|
46
46
|
return name.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
47
47
|
}
|
|
48
|
-
if (name.startsWith(
|
|
48
|
+
if (name.startsWith('__') && name.endsWith('__')) {
|
|
49
49
|
return name.slice(2, -2);
|
|
50
50
|
}
|
|
51
51
|
return name;
|
|
@@ -74,7 +74,7 @@ export async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
finally {
|
|
77
|
-
if (typeof prompter.close ===
|
|
77
|
+
if (typeof prompter.close === 'function') {
|
|
78
78
|
prompter.close();
|
|
79
79
|
}
|
|
80
80
|
}
|
package/esm/template/replace.js
CHANGED
|
@@ -31,7 +31,10 @@ async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, s
|
|
|
31
31
|
const entries = fs.readdirSync(currentSource, { withFileTypes: true });
|
|
32
32
|
for (const entry of entries) {
|
|
33
33
|
const sourceEntryPath = path.join(currentSource, entry.name);
|
|
34
|
-
|
|
34
|
+
// Skip template configuration files - they should not be copied to output
|
|
35
|
+
if (entry.name === '.boilerplate.json' ||
|
|
36
|
+
entry.name === '.boilerplate.js' ||
|
|
37
|
+
entry.name === '.boilerplates.json') {
|
|
35
38
|
continue;
|
|
36
39
|
}
|
|
37
40
|
let newName = entry.name;
|
|
@@ -62,8 +65,8 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
62
65
|
console.warn(`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`);
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
65
|
-
const author = findLicenseAuthor(answers) ??
|
|
66
|
-
const email = findLicenseEmail(answers) ??
|
|
68
|
+
const author = findLicenseAuthor(answers) ?? 'Unknown Author';
|
|
69
|
+
const email = findLicenseEmail(answers) ?? '';
|
|
67
70
|
const content = renderLicense(selectedLicense, {
|
|
68
71
|
author: String(author),
|
|
69
72
|
email: String(email || ''),
|
package/index.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { extractVariables } from
|
|
2
|
-
import { promptUser } from
|
|
3
|
-
import { replaceVariables } from
|
|
4
|
-
import { CreateGenOptions } from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
11
|
-
export * from
|
|
12
|
-
export * from
|
|
13
|
-
export * from
|
|
14
|
-
export * from
|
|
15
|
-
export * from
|
|
16
|
-
export * from
|
|
1
|
+
import { extractVariables } from './template/extract';
|
|
2
|
+
import { promptUser } from './template/prompt';
|
|
3
|
+
import { replaceVariables } from './template/replace';
|
|
4
|
+
import { CreateGenOptions } from './types';
|
|
5
|
+
export * from './cache/cache-manager';
|
|
6
|
+
export * from './cache/types';
|
|
7
|
+
export * from './git/git-cloner';
|
|
8
|
+
export * from './git/types';
|
|
9
|
+
export * from './template/templatizer';
|
|
10
|
+
export * from './template/types';
|
|
11
|
+
export * from './utils/npm-version-check';
|
|
12
|
+
export * from './utils/types';
|
|
13
|
+
export * from './template/extract';
|
|
14
|
+
export * from './template/prompt';
|
|
15
|
+
export * from './template/replace';
|
|
16
|
+
export * from './types';
|
|
17
17
|
export { extractVariables, promptUser, replaceVariables };
|
|
18
18
|
/**
|
|
19
19
|
* @deprecated This function is deprecated and will be removed in the next major version.
|
package/licenses.js
CHANGED
|
@@ -44,18 +44,18 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const PLACEHOLDER_PATTERN = /{{(\w+)}}/g;
|
|
46
46
|
let cachedTemplates = null;
|
|
47
|
-
exports.LICENSE_VALUE_KEYS = [
|
|
47
|
+
exports.LICENSE_VALUE_KEYS = ['LICENSE', 'license'];
|
|
48
48
|
exports.LICENSE_AUTHOR_KEYS = [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
'USERFULLNAME',
|
|
50
|
+
'AUTHOR',
|
|
51
|
+
'AUTHORFULLNAME',
|
|
52
|
+
'USERNAME',
|
|
53
|
+
'fullName',
|
|
54
|
+
'author',
|
|
55
|
+
'authorFullName',
|
|
56
|
+
'userName',
|
|
57
57
|
];
|
|
58
|
-
exports.LICENSE_EMAIL_KEYS = [
|
|
58
|
+
exports.LICENSE_EMAIL_KEYS = ['USEREMAIL', 'EMAIL', 'email', 'userEmail'];
|
|
59
59
|
function isSupportedLicense(name) {
|
|
60
60
|
if (!name) {
|
|
61
61
|
return false;
|
|
@@ -73,18 +73,18 @@ function renderLicense(licenseName, context) {
|
|
|
73
73
|
}
|
|
74
74
|
const ctx = {
|
|
75
75
|
year: context.year ?? new Date().getFullYear().toString(),
|
|
76
|
-
author: context.author ??
|
|
77
|
-
email: context.email ??
|
|
76
|
+
author: context.author ?? 'Unknown Author',
|
|
77
|
+
email: context.email ?? '',
|
|
78
78
|
};
|
|
79
|
-
const emailLine = ctx.email ? ` <${ctx.email}>` :
|
|
79
|
+
const emailLine = ctx.email ? ` <${ctx.email}>` : '';
|
|
80
80
|
return template.replace(PLACEHOLDER_PATTERN, (_, rawKey) => {
|
|
81
81
|
const key = rawKey.toUpperCase();
|
|
82
|
-
if (key ===
|
|
82
|
+
if (key === 'EMAIL_LINE') {
|
|
83
83
|
return emailLine;
|
|
84
84
|
}
|
|
85
85
|
const normalizedKey = key.toLowerCase();
|
|
86
86
|
const value = ctx[normalizedKey];
|
|
87
|
-
return value ||
|
|
87
|
+
return value || '';
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
function listSupportedLicenses() {
|
|
@@ -116,19 +116,19 @@ function loadLicenseTemplates() {
|
|
|
116
116
|
if (!stats.isFile()) {
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
|
-
if (path.extname(file).toLowerCase() !==
|
|
119
|
+
if (path.extname(file).toLowerCase() !== '.txt') {
|
|
120
120
|
continue;
|
|
121
121
|
}
|
|
122
122
|
const key = path.basename(file, path.extname(file)).toUpperCase();
|
|
123
|
-
templates[key] = fs.readFileSync(fullPath,
|
|
123
|
+
templates[key] = fs.readFileSync(fullPath, 'utf8');
|
|
124
124
|
}
|
|
125
125
|
cachedTemplates = templates;
|
|
126
126
|
return cachedTemplates;
|
|
127
127
|
}
|
|
128
128
|
function findTemplatesDir() {
|
|
129
129
|
const candidates = [
|
|
130
|
-
path.resolve(__dirname,
|
|
131
|
-
path.resolve(__dirname,
|
|
130
|
+
path.resolve(__dirname, '..', 'licenses-templates'),
|
|
131
|
+
path.resolve(__dirname, 'licenses-templates'),
|
|
132
132
|
];
|
|
133
133
|
for (const candidate of candidates) {
|
|
134
134
|
if (fs.existsSync(candidate)) {
|
|
@@ -140,7 +140,7 @@ function findTemplatesDir() {
|
|
|
140
140
|
function getAnswerValue(answers, keys) {
|
|
141
141
|
for (const key of keys) {
|
|
142
142
|
const value = answers?.[key];
|
|
143
|
-
if (typeof value ===
|
|
143
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
144
144
|
return value;
|
|
145
145
|
}
|
|
146
146
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gen-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"author": "Constructive <
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Clone and customize template repositories with variable replacement",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"module": "esm/index.js",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"test:watch": "jest --watch"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"appstash": "0.2.
|
|
32
|
-
"inquirerer": "2.1.
|
|
31
|
+
"appstash": "0.2.6",
|
|
32
|
+
"inquirerer": "2.1.14"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"copyfiles": "^2.4.1",
|
|
36
36
|
"makage": "0.1.8"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [],
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "9aac458b3809f4f800af22120337291041fe2144"
|
|
40
40
|
}
|
package/template/extract.d.ts
CHANGED
package/template/extract.js
CHANGED
|
@@ -36,11 +36,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.extractVariables = extractVariables;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
const PLACEHOLDER_BOUNDARY =
|
|
39
|
+
const PLACEHOLDER_BOUNDARY = '____';
|
|
40
40
|
/**
|
|
41
41
|
* Pattern to match ____variable____ in filenames and content
|
|
42
42
|
*/
|
|
43
|
-
const VARIABLE_PATTERN = new RegExp(`${PLACEHOLDER_BOUNDARY}([A-Za-z_][A-Za-z0-9_]*)${PLACEHOLDER_BOUNDARY}`,
|
|
43
|
+
const VARIABLE_PATTERN = new RegExp(`${PLACEHOLDER_BOUNDARY}([A-Za-z_][A-Za-z0-9_]*)${PLACEHOLDER_BOUNDARY}`, 'g');
|
|
44
44
|
/**
|
|
45
45
|
* Extract all variables from a template directory
|
|
46
46
|
* @param templateDir - Path to the template directory
|
|
@@ -54,8 +54,10 @@ async function extractVariables(templateDir) {
|
|
|
54
54
|
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
55
55
|
await walkDirectory(templateDir, async (filePath) => {
|
|
56
56
|
const relativePath = path.relative(templateDir, filePath);
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Skip boilerplate configuration files from variable extraction
|
|
58
|
+
if (relativePath === '.boilerplate.json' ||
|
|
59
|
+
relativePath === '.boilerplate.js' ||
|
|
60
|
+
relativePath === '.boilerplates.json') {
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
const matches = relativePath.matchAll(VARIABLE_PATTERN);
|
|
@@ -65,7 +67,7 @@ async function extractVariables(templateDir) {
|
|
|
65
67
|
fileReplacerVars.add(varName);
|
|
66
68
|
fileReplacers.push({
|
|
67
69
|
variable: varName,
|
|
68
|
-
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
|
|
70
|
+
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`, 'g'),
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -75,7 +77,7 @@ async function extractVariables(templateDir) {
|
|
|
75
77
|
contentReplacerVars.add(varName);
|
|
76
78
|
contentReplacers.push({
|
|
77
79
|
variable: varName,
|
|
78
|
-
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
|
|
80
|
+
pattern: new RegExp(`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`, 'g'),
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -94,12 +96,12 @@ async function extractVariables(templateDir) {
|
|
|
94
96
|
async function extractFromFileContent(filePath) {
|
|
95
97
|
const variables = new Set();
|
|
96
98
|
return new Promise((resolve) => {
|
|
97
|
-
const stream = fs.createReadStream(filePath, { encoding:
|
|
98
|
-
let buffer =
|
|
99
|
-
stream.on(
|
|
99
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
100
|
+
let buffer = '';
|
|
101
|
+
stream.on('data', (chunk) => {
|
|
100
102
|
buffer += chunk.toString();
|
|
101
|
-
const lines = buffer.split(
|
|
102
|
-
buffer = lines.pop() ||
|
|
103
|
+
const lines = buffer.split('\n');
|
|
104
|
+
buffer = lines.pop() || ''; // Keep the last incomplete line in buffer
|
|
103
105
|
for (const line of lines) {
|
|
104
106
|
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
105
107
|
for (const match of matches) {
|
|
@@ -107,14 +109,14 @@ async function extractFromFileContent(filePath) {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
});
|
|
110
|
-
stream.on(
|
|
112
|
+
stream.on('end', () => {
|
|
111
113
|
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
112
114
|
for (const match of matches) {
|
|
113
115
|
variables.add(match[1]);
|
|
114
116
|
}
|
|
115
117
|
resolve(variables);
|
|
116
118
|
});
|
|
117
|
-
stream.on(
|
|
119
|
+
stream.on('error', () => {
|
|
118
120
|
resolve(variables);
|
|
119
121
|
});
|
|
120
122
|
});
|
|
@@ -137,33 +139,37 @@ async function walkDirectory(dir, callback) {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
/**
|
|
140
|
-
* Load project questions from .
|
|
142
|
+
* Load project questions from .boilerplate.json or .boilerplate.js
|
|
141
143
|
* @param templateDir - Path to the template directory
|
|
142
144
|
* @returns Questions object or null if not found
|
|
143
145
|
*/
|
|
144
146
|
async function loadProjectQuestions(templateDir) {
|
|
145
|
-
const jsonPath = path.join(templateDir,
|
|
147
|
+
const jsonPath = path.join(templateDir, '.boilerplate.json');
|
|
146
148
|
if (fs.existsSync(jsonPath)) {
|
|
147
149
|
try {
|
|
148
|
-
const content = fs.readFileSync(jsonPath,
|
|
149
|
-
const
|
|
150
|
+
const content = fs.readFileSync(jsonPath, 'utf8');
|
|
151
|
+
const parsed = JSON.parse(content);
|
|
152
|
+
// .boilerplate.json has { questions: [...] } structure
|
|
153
|
+
const questions = parsed.questions ? { questions: parsed.questions } : parsed;
|
|
150
154
|
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
151
155
|
}
|
|
152
156
|
catch (error) {
|
|
153
157
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
-
console.warn(`Failed to parse .
|
|
158
|
+
console.warn(`Failed to parse .boilerplate.json: ${errorMessage}`);
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
|
-
const jsPath = path.join(templateDir,
|
|
161
|
+
const jsPath = path.join(templateDir, '.boilerplate.js');
|
|
158
162
|
if (fs.existsSync(jsPath)) {
|
|
159
163
|
try {
|
|
160
164
|
const module = require(jsPath);
|
|
161
|
-
const
|
|
165
|
+
const exported = module.default || module;
|
|
166
|
+
// .boilerplate.js can export { questions: [...] } or just the questions array
|
|
167
|
+
const questions = exported.questions ? { questions: exported.questions } : exported;
|
|
162
168
|
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
|
|
163
169
|
}
|
|
164
170
|
catch (error) {
|
|
165
171
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
166
|
-
console.warn(`Failed to load .
|
|
172
|
+
console.warn(`Failed to load .boilerplate.js: ${errorMessage}`);
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
return null;
|
|
@@ -188,5 +194,5 @@ function normalizeQuestions(questions) {
|
|
|
188
194
|
* @returns True if valid, false otherwise
|
|
189
195
|
*/
|
|
190
196
|
function validateQuestions(obj) {
|
|
191
|
-
return obj && typeof obj ===
|
|
197
|
+
return obj && typeof obj === 'object' && Array.isArray(obj.questions);
|
|
192
198
|
}
|
package/template/prompt.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Question } from
|
|
2
|
-
import { ExtractedVariables } from
|
|
1
|
+
import { Question } from 'inquirerer';
|
|
2
|
+
import { ExtractedVariables } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Generate questions from extracted variables
|
|
5
5
|
* @param extractedVariables - Variables extracted from the template
|
package/template/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.generateQuestions = generateQuestions;
|
|
4
4
|
exports.promptUser = promptUser;
|
|
5
5
|
const inquirerer_1 = require("inquirerer");
|
|
6
|
-
const PLACEHOLDER_BOUNDARY =
|
|
6
|
+
const PLACEHOLDER_BOUNDARY = '____';
|
|
7
7
|
/**
|
|
8
8
|
* Generate questions from extracted variables
|
|
9
9
|
* @param extractedVariables - Variables extracted from the template
|
|
@@ -24,7 +24,7 @@ function generateQuestions(extractedVariables) {
|
|
|
24
24
|
if (!askedVariables.has(replacer.variable)) {
|
|
25
25
|
questions.push({
|
|
26
26
|
name: replacer.variable,
|
|
27
|
-
type:
|
|
27
|
+
type: 'text',
|
|
28
28
|
message: `Enter value for ${replacer.variable}:`,
|
|
29
29
|
required: true
|
|
30
30
|
});
|
|
@@ -35,7 +35,7 @@ function generateQuestions(extractedVariables) {
|
|
|
35
35
|
if (!askedVariables.has(replacer.variable)) {
|
|
36
36
|
questions.push({
|
|
37
37
|
name: replacer.variable,
|
|
38
|
-
type:
|
|
38
|
+
type: 'text',
|
|
39
39
|
message: `Enter value for ${replacer.variable}:`,
|
|
40
40
|
required: true
|
|
41
41
|
});
|
|
@@ -49,7 +49,7 @@ function normalizeQuestionName(name) {
|
|
|
49
49
|
name.endsWith(PLACEHOLDER_BOUNDARY)) {
|
|
50
50
|
return name.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
51
51
|
}
|
|
52
|
-
if (name.startsWith(
|
|
52
|
+
if (name.startsWith('__') && name.endsWith('__')) {
|
|
53
53
|
return name.slice(2, -2);
|
|
54
54
|
}
|
|
55
55
|
return name;
|
|
@@ -78,7 +78,7 @@ async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
finally {
|
|
81
|
-
if (typeof prompter.close ===
|
|
81
|
+
if (typeof prompter.close === 'function') {
|
|
82
82
|
prompter.close();
|
|
83
83
|
}
|
|
84
84
|
}
|
package/template/replace.js
CHANGED
|
@@ -67,7 +67,10 @@ async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, s
|
|
|
67
67
|
const entries = fs.readdirSync(currentSource, { withFileTypes: true });
|
|
68
68
|
for (const entry of entries) {
|
|
69
69
|
const sourceEntryPath = path.join(currentSource, entry.name);
|
|
70
|
-
|
|
70
|
+
// Skip template configuration files - they should not be copied to output
|
|
71
|
+
if (entry.name === '.boilerplate.json' ||
|
|
72
|
+
entry.name === '.boilerplate.js' ||
|
|
73
|
+
entry.name === '.boilerplates.json') {
|
|
71
74
|
continue;
|
|
72
75
|
}
|
|
73
76
|
let newName = entry.name;
|
|
@@ -98,8 +101,8 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
98
101
|
console.warn(`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`);
|
|
99
102
|
return;
|
|
100
103
|
}
|
|
101
|
-
const author = (0, licenses_1.findLicenseAuthor)(answers) ??
|
|
102
|
-
const email = (0, licenses_1.findLicenseEmail)(answers) ??
|
|
104
|
+
const author = (0, licenses_1.findLicenseAuthor)(answers) ?? 'Unknown Author';
|
|
105
|
+
const email = (0, licenses_1.findLicenseEmail)(answers) ?? '';
|
|
103
106
|
const content = (0, licenses_1.renderLicense)(selectedLicense, {
|
|
104
107
|
author: String(author),
|
|
105
108
|
email: String(email || ''),
|
package/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Question } from 'inquirerer';
|
|
2
2
|
/**
|
|
3
|
-
* Questions configuration that can be loaded from .
|
|
3
|
+
* Questions configuration that can be loaded from .boilerplate.json or .boilerplate.js
|
|
4
4
|
* @typedef {Object} Questions
|
|
5
5
|
* @property {Question[]} questions - Array of inquirerer questions
|
|
6
6
|
*/
|