create-gen-app 0.2.2 → 0.3.1
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/esm/licenses.js +30 -0
- package/esm/template/extract.js +4 -80
- package/esm/template/prompt.js +4 -88
- package/esm/template/replace.js +4 -22
- package/licenses.d.ts +6 -0
- package/licenses.js +34 -0
- package/package.json +3 -3
- package/template/extract.js +4 -80
- package/template/prompt.js +4 -88
- package/template/replace.js +3 -21
- package/types.d.ts +0 -1
- /package/{APACHE-2.0.txt → licenses-templates/APACHE-2.0.txt} +0 -0
- /package/{BSD-3-CLAUSE.txt → licenses-templates/BSD-3-CLAUSE.txt} +0 -0
- /package/{GPL-3.0.txt → licenses-templates/GPL-3.0.txt} +0 -0
- /package/{ISC.txt → licenses-templates/ISC.txt} +0 -0
- /package/{MIT.txt → licenses-templates/MIT.txt} +0 -0
- /package/{MPL-2.0.txt → licenses-templates/MPL-2.0.txt} +0 -0
- /package/{UNLICENSE.txt → licenses-templates/UNLICENSE.txt} +0 -0
package/esm/licenses.js
CHANGED
|
@@ -2,6 +2,18 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
const PLACEHOLDER_PATTERN = /{{(\w+)}}/g;
|
|
4
4
|
let cachedTemplates = null;
|
|
5
|
+
export const LICENSE_VALUE_KEYS = ["LICENSE", "license"];
|
|
6
|
+
export const LICENSE_AUTHOR_KEYS = [
|
|
7
|
+
"USERFULLNAME",
|
|
8
|
+
"AUTHOR",
|
|
9
|
+
"AUTHORFULLNAME",
|
|
10
|
+
"USERNAME",
|
|
11
|
+
"fullName",
|
|
12
|
+
"author",
|
|
13
|
+
"authorFullName",
|
|
14
|
+
"userName",
|
|
15
|
+
];
|
|
16
|
+
export const LICENSE_EMAIL_KEYS = ["USEREMAIL", "EMAIL", "email", "userEmail"];
|
|
5
17
|
export function isSupportedLicense(name) {
|
|
6
18
|
if (!name) {
|
|
7
19
|
return false;
|
|
@@ -36,6 +48,15 @@ export function renderLicense(licenseName, context) {
|
|
|
36
48
|
export function listSupportedLicenses() {
|
|
37
49
|
return Object.keys(loadLicenseTemplates());
|
|
38
50
|
}
|
|
51
|
+
export function findLicenseValue(answers) {
|
|
52
|
+
return getAnswerValue(answers, LICENSE_VALUE_KEYS);
|
|
53
|
+
}
|
|
54
|
+
export function findLicenseAuthor(answers) {
|
|
55
|
+
return getAnswerValue(answers, LICENSE_AUTHOR_KEYS);
|
|
56
|
+
}
|
|
57
|
+
export function findLicenseEmail(answers) {
|
|
58
|
+
return getAnswerValue(answers, LICENSE_EMAIL_KEYS);
|
|
59
|
+
}
|
|
39
60
|
function loadLicenseTemplates() {
|
|
40
61
|
if (cachedTemplates) {
|
|
41
62
|
return cachedTemplates;
|
|
@@ -74,3 +95,12 @@ function findTemplatesDir() {
|
|
|
74
95
|
}
|
|
75
96
|
return null;
|
|
76
97
|
}
|
|
98
|
+
function getAnswerValue(answers, keys) {
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
const value = answers?.[key];
|
|
101
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
package/esm/template/extract.js
CHANGED
|
@@ -16,13 +16,8 @@ export async function extractVariables(templateDir) {
|
|
|
16
16
|
const fileReplacerVars = new Set();
|
|
17
17
|
const contentReplacerVars = new Set();
|
|
18
18
|
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
19
|
-
const ignoredPathPatterns = new Set(projectQuestions?.ignore ?? []);
|
|
20
|
-
const ignoredContentTokens = buildIgnoredContentTokens(projectQuestions);
|
|
21
19
|
await walkDirectory(templateDir, async (filePath) => {
|
|
22
20
|
const relativePath = path.relative(templateDir, filePath);
|
|
23
|
-
if (shouldIgnore(relativePath, ignoredPathPatterns)) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
21
|
if (relativePath === ".questions.json" ||
|
|
27
22
|
relativePath === ".questions.js") {
|
|
28
23
|
return;
|
|
@@ -38,7 +33,7 @@ export async function extractVariables(templateDir) {
|
|
|
38
33
|
});
|
|
39
34
|
}
|
|
40
35
|
}
|
|
41
|
-
const contentVars = await extractFromFileContent(filePath
|
|
36
|
+
const contentVars = await extractFromFileContent(filePath);
|
|
42
37
|
for (const varName of contentVars) {
|
|
43
38
|
if (!contentReplacerVars.has(varName)) {
|
|
44
39
|
contentReplacerVars.add(varName);
|
|
@@ -60,7 +55,7 @@ export async function extractVariables(templateDir) {
|
|
|
60
55
|
* @param filePath - Path to the file
|
|
61
56
|
* @returns Set of variable names found in the file
|
|
62
57
|
*/
|
|
63
|
-
async function extractFromFileContent(filePath
|
|
58
|
+
async function extractFromFileContent(filePath) {
|
|
64
59
|
const variables = new Set();
|
|
65
60
|
return new Promise((resolve) => {
|
|
66
61
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
@@ -72,18 +67,14 @@ async function extractFromFileContent(filePath, ignoredTokens) {
|
|
|
72
67
|
for (const line of lines) {
|
|
73
68
|
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
74
69
|
for (const match of matches) {
|
|
75
|
-
|
|
76
|
-
variables.add(match[1]);
|
|
77
|
-
}
|
|
70
|
+
variables.add(match[1]);
|
|
78
71
|
}
|
|
79
72
|
}
|
|
80
73
|
});
|
|
81
74
|
stream.on("end", () => {
|
|
82
75
|
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
83
76
|
for (const match of matches) {
|
|
84
|
-
|
|
85
|
-
variables.add(match[1]);
|
|
86
|
-
}
|
|
77
|
+
variables.add(match[1]);
|
|
87
78
|
}
|
|
88
79
|
resolve(variables);
|
|
89
80
|
});
|
|
@@ -141,73 +132,6 @@ async function loadProjectQuestions(templateDir) {
|
|
|
141
132
|
}
|
|
142
133
|
return null;
|
|
143
134
|
}
|
|
144
|
-
function shouldIgnore(relativePath, ignoredPatterns) {
|
|
145
|
-
if (ignoredPatterns.size === 0) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
const normalized = relativePath.split(path.sep).join("/");
|
|
149
|
-
const segments = normalized.split("/");
|
|
150
|
-
for (const pattern of ignoredPatterns) {
|
|
151
|
-
const normalizedPattern = pattern.split(path.sep).join("/");
|
|
152
|
-
if (normalizedPattern === normalized) {
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
if (!normalizedPattern.includes("/")) {
|
|
156
|
-
if (segments.includes(normalizedPattern)) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (normalizedPattern.startsWith("**/")) {
|
|
162
|
-
const suffix = normalizedPattern.slice(3);
|
|
163
|
-
if (normalized.endsWith(suffix)) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (normalizedPattern.endsWith("/**")) {
|
|
169
|
-
const prefix = normalizedPattern.slice(0, -3);
|
|
170
|
-
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (normalized.startsWith(`${normalizedPattern}/`)) {
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
const DEFAULT_IGNORED_CONTENT_TOKENS = ["tests", "snapshots"];
|
|
182
|
-
function shouldIgnoreContent(match, ignoredTokens) {
|
|
183
|
-
if (ignoredTokens.size === 0) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
const token = match.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
187
|
-
return ignoredTokens.has(token);
|
|
188
|
-
}
|
|
189
|
-
function buildIgnoredContentTokens(projectQuestions) {
|
|
190
|
-
const tokens = new Set(DEFAULT_IGNORED_CONTENT_TOKENS);
|
|
191
|
-
if (projectQuestions?.ignore) {
|
|
192
|
-
for (const entry of projectQuestions.ignore) {
|
|
193
|
-
const normalized = normalizePlaceholder(entry);
|
|
194
|
-
if (normalized) {
|
|
195
|
-
tokens.add(normalized);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return tokens;
|
|
200
|
-
}
|
|
201
|
-
function normalizePlaceholder(entry) {
|
|
202
|
-
if (entry.startsWith(PLACEHOLDER_BOUNDARY) &&
|
|
203
|
-
entry.endsWith(PLACEHOLDER_BOUNDARY)) {
|
|
204
|
-
return entry.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
205
|
-
}
|
|
206
|
-
if (entry.startsWith("__") && entry.endsWith("__")) {
|
|
207
|
-
return entry.slice(2, -2);
|
|
208
|
-
}
|
|
209
|
-
return entry || null;
|
|
210
|
-
}
|
|
211
135
|
/**
|
|
212
136
|
* Normalize questions by ensuring all required fields have default values
|
|
213
137
|
* @param questions - Questions object to normalize
|
package/esm/template/prompt.js
CHANGED
|
@@ -68,11 +68,10 @@ export async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
|
68
68
|
});
|
|
69
69
|
try {
|
|
70
70
|
const promptAnswers = await prompter.prompt(preparedArgv, questions);
|
|
71
|
-
|
|
71
|
+
return {
|
|
72
72
|
...argv,
|
|
73
73
|
...promptAnswers,
|
|
74
74
|
};
|
|
75
|
-
return expandAnswersForVariables(mergedAnswers, extractedVariables);
|
|
76
75
|
}
|
|
77
76
|
finally {
|
|
78
77
|
if (typeof prompter.close === "function") {
|
|
@@ -91,93 +90,10 @@ function mapArgvToQuestions(argv, questions) {
|
|
|
91
90
|
if (prepared[name] !== undefined) {
|
|
92
91
|
continue;
|
|
93
92
|
}
|
|
94
|
-
const
|
|
95
|
-
if (matchValue !== null) {
|
|
96
|
-
prepared[name] = matchValue;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return prepared;
|
|
100
|
-
}
|
|
101
|
-
function findMatchingArgValue(targetName, argvEntries) {
|
|
102
|
-
const normalizedTarget = normalizeIdentifier(targetName);
|
|
103
|
-
for (const [key, value] of argvEntries) {
|
|
104
|
-
if (value === undefined || value === null) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (key === targetName) {
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
const normalizedKey = normalizeIdentifier(key);
|
|
111
|
-
if (identifiersMatch(normalizedTarget, normalizedKey)) {
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
function normalizeIdentifier(value) {
|
|
118
|
-
return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
119
|
-
}
|
|
120
|
-
function identifiersMatch(a, b) {
|
|
121
|
-
if (a === b) {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
if (a.includes(b) || b.includes(a)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
return hasSignificantOverlap(a, b);
|
|
128
|
-
}
|
|
129
|
-
function hasSignificantOverlap(a, b) {
|
|
130
|
-
const minLength = 4;
|
|
131
|
-
if (a.length < minLength || b.length < minLength) {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
const shorter = a.length <= b.length ? a : b;
|
|
135
|
-
const longer = shorter === a ? b : a;
|
|
136
|
-
for (let i = 0; i <= shorter.length - minLength; i++) {
|
|
137
|
-
const slice = shorter.slice(i, i + minLength);
|
|
138
|
-
if (longer.includes(slice)) {
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
function expandAnswersForVariables(answers, extractedVariables) {
|
|
145
|
-
const expanded = { ...answers };
|
|
146
|
-
const variables = collectVariableNames(extractedVariables);
|
|
147
|
-
const answerEntries = Object.entries(expanded).map(([key, value]) => ({
|
|
148
|
-
key,
|
|
149
|
-
value,
|
|
150
|
-
normalized: normalizeIdentifier(key),
|
|
151
|
-
}));
|
|
152
|
-
for (const variable of variables) {
|
|
153
|
-
if (expanded[variable] !== undefined) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
const normalizedVar = normalizeIdentifier(variable);
|
|
157
|
-
const match = answerEntries.find(({ normalized }) => identifiersMatch(normalizedVar, normalized));
|
|
93
|
+
const match = argvEntries.find(([key]) => key === name);
|
|
158
94
|
if (match) {
|
|
159
|
-
|
|
160
|
-
answerEntries.push({
|
|
161
|
-
key: variable,
|
|
162
|
-
value: match.value,
|
|
163
|
-
normalized: normalizedVar,
|
|
164
|
-
});
|
|
95
|
+
prepared[name] = match[1];
|
|
165
96
|
}
|
|
166
97
|
}
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
function collectVariableNames(extractedVariables) {
|
|
170
|
-
const names = new Set();
|
|
171
|
-
for (const replacer of extractedVariables.fileReplacers) {
|
|
172
|
-
names.add(replacer.variable);
|
|
173
|
-
}
|
|
174
|
-
for (const replacer of extractedVariables.contentReplacers) {
|
|
175
|
-
names.add(replacer.variable);
|
|
176
|
-
}
|
|
177
|
-
if (extractedVariables.projectQuestions) {
|
|
178
|
-
for (const question of extractedVariables.projectQuestions.questions) {
|
|
179
|
-
names.add(normalizeQuestionName(question.name));
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return names;
|
|
98
|
+
return prepared;
|
|
183
99
|
}
|
package/esm/template/replace.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { Transform } from 'stream';
|
|
4
4
|
import { pipeline } from 'stream/promises';
|
|
5
|
-
import { renderLicense, isSupportedLicense } from '../licenses';
|
|
5
|
+
import { renderLicense, isSupportedLicense, findLicenseAuthor, findLicenseEmail, findLicenseValue, } from '../licenses';
|
|
6
6
|
/**
|
|
7
7
|
* Replace variables in all files in the template directory
|
|
8
8
|
* @param templateDir - Path to the template directory
|
|
@@ -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 =
|
|
56
|
+
const licenseValue = findLicenseValue(answers);
|
|
57
57
|
if (typeof licenseValue !== 'string' || licenseValue.trim() === '') {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
@@ -62,17 +62,8 @@ 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 =
|
|
66
|
-
|
|
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"]) ?? "";
|
|
65
|
+
const author = findLicenseAuthor(answers) ?? "Unknown Author";
|
|
66
|
+
const email = findLicenseEmail(answers) ?? "";
|
|
76
67
|
const content = renderLicense(selectedLicense, {
|
|
77
68
|
author: String(author),
|
|
78
69
|
email: String(email || ''),
|
|
@@ -85,15 +76,6 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
85
76
|
fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
|
|
86
77
|
console.log(`[create-gen-app] LICENSE updated with ${selectedLicense} template.`);
|
|
87
78
|
}
|
|
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
|
-
}
|
|
97
79
|
/**
|
|
98
80
|
* Replace variables in a file using streams
|
|
99
81
|
* @param sourcePath - Source file path
|
package/licenses.d.ts
CHANGED
|
@@ -4,7 +4,13 @@ interface LicenseContext {
|
|
|
4
4
|
email: string;
|
|
5
5
|
}
|
|
6
6
|
export type SupportedLicense = string;
|
|
7
|
+
export declare const LICENSE_VALUE_KEYS: string[];
|
|
8
|
+
export declare const LICENSE_AUTHOR_KEYS: string[];
|
|
9
|
+
export declare const LICENSE_EMAIL_KEYS: string[];
|
|
7
10
|
export declare function isSupportedLicense(name: string): name is SupportedLicense;
|
|
8
11
|
export declare function renderLicense(licenseName: string, context: Partial<LicenseContext>): string | null;
|
|
9
12
|
export declare function listSupportedLicenses(): string[];
|
|
13
|
+
export declare function findLicenseValue(answers: Record<string, any>): string | undefined;
|
|
14
|
+
export declare function findLicenseAuthor(answers: Record<string, any>): string | undefined;
|
|
15
|
+
export declare function findLicenseEmail(answers: Record<string, any>): string | undefined;
|
|
10
16
|
export {};
|
package/licenses.js
CHANGED
|
@@ -33,13 +33,29 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LICENSE_EMAIL_KEYS = exports.LICENSE_AUTHOR_KEYS = exports.LICENSE_VALUE_KEYS = void 0;
|
|
36
37
|
exports.isSupportedLicense = isSupportedLicense;
|
|
37
38
|
exports.renderLicense = renderLicense;
|
|
38
39
|
exports.listSupportedLicenses = listSupportedLicenses;
|
|
40
|
+
exports.findLicenseValue = findLicenseValue;
|
|
41
|
+
exports.findLicenseAuthor = findLicenseAuthor;
|
|
42
|
+
exports.findLicenseEmail = findLicenseEmail;
|
|
39
43
|
const fs = __importStar(require("fs"));
|
|
40
44
|
const path = __importStar(require("path"));
|
|
41
45
|
const PLACEHOLDER_PATTERN = /{{(\w+)}}/g;
|
|
42
46
|
let cachedTemplates = null;
|
|
47
|
+
exports.LICENSE_VALUE_KEYS = ["LICENSE", "license"];
|
|
48
|
+
exports.LICENSE_AUTHOR_KEYS = [
|
|
49
|
+
"USERFULLNAME",
|
|
50
|
+
"AUTHOR",
|
|
51
|
+
"AUTHORFULLNAME",
|
|
52
|
+
"USERNAME",
|
|
53
|
+
"fullName",
|
|
54
|
+
"author",
|
|
55
|
+
"authorFullName",
|
|
56
|
+
"userName",
|
|
57
|
+
];
|
|
58
|
+
exports.LICENSE_EMAIL_KEYS = ["USEREMAIL", "EMAIL", "email", "userEmail"];
|
|
43
59
|
function isSupportedLicense(name) {
|
|
44
60
|
if (!name) {
|
|
45
61
|
return false;
|
|
@@ -74,6 +90,15 @@ function renderLicense(licenseName, context) {
|
|
|
74
90
|
function listSupportedLicenses() {
|
|
75
91
|
return Object.keys(loadLicenseTemplates());
|
|
76
92
|
}
|
|
93
|
+
function findLicenseValue(answers) {
|
|
94
|
+
return getAnswerValue(answers, exports.LICENSE_VALUE_KEYS);
|
|
95
|
+
}
|
|
96
|
+
function findLicenseAuthor(answers) {
|
|
97
|
+
return getAnswerValue(answers, exports.LICENSE_AUTHOR_KEYS);
|
|
98
|
+
}
|
|
99
|
+
function findLicenseEmail(answers) {
|
|
100
|
+
return getAnswerValue(answers, exports.LICENSE_EMAIL_KEYS);
|
|
101
|
+
}
|
|
77
102
|
function loadLicenseTemplates() {
|
|
78
103
|
if (cachedTemplates) {
|
|
79
104
|
return cachedTemplates;
|
|
@@ -112,3 +137,12 @@ function findTemplatesDir() {
|
|
|
112
137
|
}
|
|
113
138
|
return null;
|
|
114
139
|
}
|
|
140
|
+
function getAnswerValue(answers, keys) {
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
const value = answers?.[key];
|
|
143
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-gen-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
5
|
"description": "Clone and customize template repositories with variable replacement",
|
|
6
6
|
"main": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/hyperweb-io/dev-utils/issues"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"copy": "copyfiles -f ../../LICENSE README.md package.json \"licenses-templates/**/*\" dist",
|
|
23
|
+
"copy": "copyfiles -f ../../LICENSE README.md package.json dist && copyfiles -f \"licenses-templates/**/*\" dist/licenses-templates",
|
|
24
24
|
"clean": "makage clean",
|
|
25
25
|
"prepublishOnly": "npm run build",
|
|
26
26
|
"build": "npm run clean && tsc && tsc -p tsconfig.esm.json && npm run copy",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"makage": "0.1.8"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [],
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "3e7186aed1ddfe77a1c03d4e1ca48d7a1811b53f"
|
|
40
40
|
}
|
package/template/extract.js
CHANGED
|
@@ -52,13 +52,8 @@ async function extractVariables(templateDir) {
|
|
|
52
52
|
const fileReplacerVars = new Set();
|
|
53
53
|
const contentReplacerVars = new Set();
|
|
54
54
|
const projectQuestions = await loadProjectQuestions(templateDir);
|
|
55
|
-
const ignoredPathPatterns = new Set(projectQuestions?.ignore ?? []);
|
|
56
|
-
const ignoredContentTokens = buildIgnoredContentTokens(projectQuestions);
|
|
57
55
|
await walkDirectory(templateDir, async (filePath) => {
|
|
58
56
|
const relativePath = path.relative(templateDir, filePath);
|
|
59
|
-
if (shouldIgnore(relativePath, ignoredPathPatterns)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
57
|
if (relativePath === ".questions.json" ||
|
|
63
58
|
relativePath === ".questions.js") {
|
|
64
59
|
return;
|
|
@@ -74,7 +69,7 @@ async function extractVariables(templateDir) {
|
|
|
74
69
|
});
|
|
75
70
|
}
|
|
76
71
|
}
|
|
77
|
-
const contentVars = await extractFromFileContent(filePath
|
|
72
|
+
const contentVars = await extractFromFileContent(filePath);
|
|
78
73
|
for (const varName of contentVars) {
|
|
79
74
|
if (!contentReplacerVars.has(varName)) {
|
|
80
75
|
contentReplacerVars.add(varName);
|
|
@@ -96,7 +91,7 @@ async function extractVariables(templateDir) {
|
|
|
96
91
|
* @param filePath - Path to the file
|
|
97
92
|
* @returns Set of variable names found in the file
|
|
98
93
|
*/
|
|
99
|
-
async function extractFromFileContent(filePath
|
|
94
|
+
async function extractFromFileContent(filePath) {
|
|
100
95
|
const variables = new Set();
|
|
101
96
|
return new Promise((resolve) => {
|
|
102
97
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
@@ -108,18 +103,14 @@ async function extractFromFileContent(filePath, ignoredTokens) {
|
|
|
108
103
|
for (const line of lines) {
|
|
109
104
|
const matches = line.matchAll(VARIABLE_PATTERN);
|
|
110
105
|
for (const match of matches) {
|
|
111
|
-
|
|
112
|
-
variables.add(match[1]);
|
|
113
|
-
}
|
|
106
|
+
variables.add(match[1]);
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
});
|
|
117
110
|
stream.on("end", () => {
|
|
118
111
|
const matches = buffer.matchAll(VARIABLE_PATTERN);
|
|
119
112
|
for (const match of matches) {
|
|
120
|
-
|
|
121
|
-
variables.add(match[1]);
|
|
122
|
-
}
|
|
113
|
+
variables.add(match[1]);
|
|
123
114
|
}
|
|
124
115
|
resolve(variables);
|
|
125
116
|
});
|
|
@@ -177,73 +168,6 @@ async function loadProjectQuestions(templateDir) {
|
|
|
177
168
|
}
|
|
178
169
|
return null;
|
|
179
170
|
}
|
|
180
|
-
function shouldIgnore(relativePath, ignoredPatterns) {
|
|
181
|
-
if (ignoredPatterns.size === 0) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
const normalized = relativePath.split(path.sep).join("/");
|
|
185
|
-
const segments = normalized.split("/");
|
|
186
|
-
for (const pattern of ignoredPatterns) {
|
|
187
|
-
const normalizedPattern = pattern.split(path.sep).join("/");
|
|
188
|
-
if (normalizedPattern === normalized) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
if (!normalizedPattern.includes("/")) {
|
|
192
|
-
if (segments.includes(normalizedPattern)) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (normalizedPattern.startsWith("**/")) {
|
|
198
|
-
const suffix = normalizedPattern.slice(3);
|
|
199
|
-
if (normalized.endsWith(suffix)) {
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (normalizedPattern.endsWith("/**")) {
|
|
205
|
-
const prefix = normalizedPattern.slice(0, -3);
|
|
206
|
-
if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (normalized.startsWith(`${normalizedPattern}/`)) {
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
const DEFAULT_IGNORED_CONTENT_TOKENS = ["tests", "snapshots"];
|
|
218
|
-
function shouldIgnoreContent(match, ignoredTokens) {
|
|
219
|
-
if (ignoredTokens.size === 0) {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
const token = match.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
223
|
-
return ignoredTokens.has(token);
|
|
224
|
-
}
|
|
225
|
-
function buildIgnoredContentTokens(projectQuestions) {
|
|
226
|
-
const tokens = new Set(DEFAULT_IGNORED_CONTENT_TOKENS);
|
|
227
|
-
if (projectQuestions?.ignore) {
|
|
228
|
-
for (const entry of projectQuestions.ignore) {
|
|
229
|
-
const normalized = normalizePlaceholder(entry);
|
|
230
|
-
if (normalized) {
|
|
231
|
-
tokens.add(normalized);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return tokens;
|
|
236
|
-
}
|
|
237
|
-
function normalizePlaceholder(entry) {
|
|
238
|
-
if (entry.startsWith(PLACEHOLDER_BOUNDARY) &&
|
|
239
|
-
entry.endsWith(PLACEHOLDER_BOUNDARY)) {
|
|
240
|
-
return entry.slice(PLACEHOLDER_BOUNDARY.length, -PLACEHOLDER_BOUNDARY.length);
|
|
241
|
-
}
|
|
242
|
-
if (entry.startsWith("__") && entry.endsWith("__")) {
|
|
243
|
-
return entry.slice(2, -2);
|
|
244
|
-
}
|
|
245
|
-
return entry || null;
|
|
246
|
-
}
|
|
247
171
|
/**
|
|
248
172
|
* Normalize questions by ensuring all required fields have default values
|
|
249
173
|
* @param questions - Questions object to normalize
|
package/template/prompt.js
CHANGED
|
@@ -72,11 +72,10 @@ async function promptUser(extractedVariables, argv = {}, noTty = false) {
|
|
|
72
72
|
});
|
|
73
73
|
try {
|
|
74
74
|
const promptAnswers = await prompter.prompt(preparedArgv, questions);
|
|
75
|
-
|
|
75
|
+
return {
|
|
76
76
|
...argv,
|
|
77
77
|
...promptAnswers,
|
|
78
78
|
};
|
|
79
|
-
return expandAnswersForVariables(mergedAnswers, extractedVariables);
|
|
80
79
|
}
|
|
81
80
|
finally {
|
|
82
81
|
if (typeof prompter.close === "function") {
|
|
@@ -95,93 +94,10 @@ function mapArgvToQuestions(argv, questions) {
|
|
|
95
94
|
if (prepared[name] !== undefined) {
|
|
96
95
|
continue;
|
|
97
96
|
}
|
|
98
|
-
const
|
|
99
|
-
if (matchValue !== null) {
|
|
100
|
-
prepared[name] = matchValue;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return prepared;
|
|
104
|
-
}
|
|
105
|
-
function findMatchingArgValue(targetName, argvEntries) {
|
|
106
|
-
const normalizedTarget = normalizeIdentifier(targetName);
|
|
107
|
-
for (const [key, value] of argvEntries) {
|
|
108
|
-
if (value === undefined || value === null) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (key === targetName) {
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
const normalizedKey = normalizeIdentifier(key);
|
|
115
|
-
if (identifiersMatch(normalizedTarget, normalizedKey)) {
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
function normalizeIdentifier(value) {
|
|
122
|
-
return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
123
|
-
}
|
|
124
|
-
function identifiersMatch(a, b) {
|
|
125
|
-
if (a === b) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
if (a.includes(b) || b.includes(a)) {
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
return hasSignificantOverlap(a, b);
|
|
132
|
-
}
|
|
133
|
-
function hasSignificantOverlap(a, b) {
|
|
134
|
-
const minLength = 4;
|
|
135
|
-
if (a.length < minLength || b.length < minLength) {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
const shorter = a.length <= b.length ? a : b;
|
|
139
|
-
const longer = shorter === a ? b : a;
|
|
140
|
-
for (let i = 0; i <= shorter.length - minLength; i++) {
|
|
141
|
-
const slice = shorter.slice(i, i + minLength);
|
|
142
|
-
if (longer.includes(slice)) {
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
function expandAnswersForVariables(answers, extractedVariables) {
|
|
149
|
-
const expanded = { ...answers };
|
|
150
|
-
const variables = collectVariableNames(extractedVariables);
|
|
151
|
-
const answerEntries = Object.entries(expanded).map(([key, value]) => ({
|
|
152
|
-
key,
|
|
153
|
-
value,
|
|
154
|
-
normalized: normalizeIdentifier(key),
|
|
155
|
-
}));
|
|
156
|
-
for (const variable of variables) {
|
|
157
|
-
if (expanded[variable] !== undefined) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const normalizedVar = normalizeIdentifier(variable);
|
|
161
|
-
const match = answerEntries.find(({ normalized }) => identifiersMatch(normalizedVar, normalized));
|
|
97
|
+
const match = argvEntries.find(([key]) => key === name);
|
|
162
98
|
if (match) {
|
|
163
|
-
|
|
164
|
-
answerEntries.push({
|
|
165
|
-
key: variable,
|
|
166
|
-
value: match.value,
|
|
167
|
-
normalized: normalizedVar,
|
|
168
|
-
});
|
|
99
|
+
prepared[name] = match[1];
|
|
169
100
|
}
|
|
170
101
|
}
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
function collectVariableNames(extractedVariables) {
|
|
174
|
-
const names = new Set();
|
|
175
|
-
for (const replacer of extractedVariables.fileReplacers) {
|
|
176
|
-
names.add(replacer.variable);
|
|
177
|
-
}
|
|
178
|
-
for (const replacer of extractedVariables.contentReplacers) {
|
|
179
|
-
names.add(replacer.variable);
|
|
180
|
-
}
|
|
181
|
-
if (extractedVariables.projectQuestions) {
|
|
182
|
-
for (const question of extractedVariables.projectQuestions.questions) {
|
|
183
|
-
names.add(normalizeQuestionName(question.name));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return names;
|
|
102
|
+
return prepared;
|
|
187
103
|
}
|
package/template/replace.js
CHANGED
|
@@ -89,7 +89,7 @@ async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, s
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
async function ensureLicenseFile(outputDir, answers) {
|
|
92
|
-
const licenseValue =
|
|
92
|
+
const licenseValue = (0, licenses_1.findLicenseValue)(answers);
|
|
93
93
|
if (typeof licenseValue !== 'string' || licenseValue.trim() === '') {
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
@@ -98,17 +98,8 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
98
98
|
console.warn(`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
const author =
|
|
102
|
-
|
|
103
|
-
"AUTHOR",
|
|
104
|
-
"AUTHORFULLNAME",
|
|
105
|
-
"USERNAME",
|
|
106
|
-
"fullName",
|
|
107
|
-
"author",
|
|
108
|
-
"authorFullName",
|
|
109
|
-
"userName",
|
|
110
|
-
]) ?? "Unknown Author";
|
|
111
|
-
const email = getAnswer(answers, ["USEREMAIL", "EMAIL", "email", "userEmail"]) ?? "";
|
|
101
|
+
const author = (0, licenses_1.findLicenseAuthor)(answers) ?? "Unknown Author";
|
|
102
|
+
const email = (0, licenses_1.findLicenseEmail)(answers) ?? "";
|
|
112
103
|
const content = (0, licenses_1.renderLicense)(selectedLicense, {
|
|
113
104
|
author: String(author),
|
|
114
105
|
email: String(email || ''),
|
|
@@ -121,15 +112,6 @@ async function ensureLicenseFile(outputDir, answers) {
|
|
|
121
112
|
fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
|
|
122
113
|
console.log(`[create-gen-app] LICENSE updated with ${selectedLicense} template.`);
|
|
123
114
|
}
|
|
124
|
-
function getAnswer(answers, keys) {
|
|
125
|
-
for (const key of keys) {
|
|
126
|
-
const value = answers?.[key];
|
|
127
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
115
|
/**
|
|
134
116
|
* Replace variables in a file using streams
|
|
135
117
|
* @param sourcePath - Source file path
|
package/types.d.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|