Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.

Commit caa3e3c

Browse files
committed
feat(create-gen-app): switch placeholders to four underscores
- treat variables as ____VAR____ throughout extraction, prompts, and README - add automatic normalization for question names using ____VAR____ or legacy __VAR__ - extend ignore logic to cover content tokens by default - update unit tests for new placeholder format
1 parent 5e8e0e5 commit caa3e3c

File tree

5 files changed

+104
-49
lines changed

5 files changed

+104
-49
lines changed

packages/create-gen-app/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ A TypeScript library for cloning and customizing template repositories with vari
1919
## Features
2020

2121
- Clone GitHub repositories or any git URL
22-
- Extract template variables from filenames and file contents using `__VARIABLE__` syntax
22+
- Extract template variables from filenames and file contents using `____VARIABLE____` syntax
2323
- Load custom questions from `.questions.json` or `.questions.js` files
2424
- Interactive prompts using inquirerer with CLI argument support
2525
- Stream-based file processing for efficient variable replacement
@@ -49,19 +49,19 @@ await createGen({
4949

5050
### Template Variables
5151

52-
Variables in your template should be wrapped in double underscores:
52+
Variables in your template should be wrapped in `____` (four underscores) on both sides:
5353

5454
**Filename variables:**
5555
```
56-
__PROJECT_NAME__/
57-
__MODULE_NAME__.ts
56+
____PROJECT_NAME____/
57+
____MODULE_NAME____.ts
5858
```
5959

6060
**Content variables:**
6161
```typescript
62-
// __MODULE_NAME__.ts
63-
export const projectName = "__PROJECT_NAME__";
64-
export const author = "__AUTHOR__";
62+
// ____MODULE_NAME____.ts
63+
export const projectName = "____PROJECT_NAME____";
64+
export const author = "____AUTHOR____";
6565
```
6666

6767
### Custom Questions

packages/create-gen-app/__tests__/create-gen.test.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ describe("create-gen-app", () => {
4545
describe("extractVariables", () => {
4646
it("should extract variables from filenames", async () => {
4747
fs.writeFileSync(
48-
path.join(testTempDir, "__PROJECT_NAME__.txt"),
48+
path.join(testTempDir, "____PROJECT_NAME____.txt"),
4949
"content"
5050
);
51-
fs.writeFileSync(path.join(testTempDir, "__AUTHOR__.md"), "content");
51+
fs.writeFileSync(path.join(testTempDir, "____AUTHOR____.md"), "content");
5252

5353
const result = await extractVariables(testTempDir);
5454

@@ -62,7 +62,7 @@ describe("create-gen-app", () => {
6262
it("should extract variables from file contents", async () => {
6363
fs.writeFileSync(
6464
path.join(testTempDir, "test.txt"),
65-
"Hello __USER_NAME__, welcome to __PROJECT_NAME__!"
65+
"Hello ____USER_NAME____, welcome to ____PROJECT_NAME____!"
6666
);
6767

6868
const result = await extractVariables(testTempDir);
@@ -77,11 +77,11 @@ describe("create-gen-app", () => {
7777
});
7878

7979
it("should extract variables from nested directories", async () => {
80-
const nestedDir = path.join(testTempDir, "src", "__MODULE_NAME__");
80+
const nestedDir = path.join(testTempDir, "src", "____MODULE_NAME____");
8181
fs.mkdirSync(nestedDir, { recursive: true });
8282
fs.writeFileSync(
83-
path.join(nestedDir, "__FILE_NAME__.ts"),
84-
'export const __CONSTANT__ = "value";'
83+
path.join(nestedDir, "____FILE_NAME____.ts"),
84+
'export const ____CONSTANT____ = "value";'
8585
);
8686

8787
const result = await extractVariables(testTempDir);
@@ -163,7 +163,7 @@ module.exports = {
163163
it("should skip .questions.json and .questions.js from variable extraction", async () => {
164164
fs.writeFileSync(
165165
path.join(testTempDir, ".questions.json"),
166-
'{"questions": [{"name": "__SHOULD_NOT_EXTRACT__"}]}'
166+
'{"questions": [{"name": "____SHOULD_NOT_EXTRACT____"}]}'
167167
);
168168

169169
const result = await extractVariables(testTempDir);
@@ -176,7 +176,7 @@ module.exports = {
176176
it("should handle variables with different casings", async () => {
177177
fs.writeFileSync(
178178
path.join(testTempDir, "test.txt"),
179-
"__lowercase__ __UPPERCASE__ __CamelCase__ __snake_case__"
179+
"__lowercase__ ____UPPERCASE____ ____CamelCase____ __snake_case__"
180180
);
181181

182182
const result = await extractVariables(testTempDir);
@@ -210,9 +210,9 @@ module.exports = {
210210

211211
const extractedVariables: ExtractedVariables = {
212212
fileReplacers: [
213-
{ variable: "PROJECT_NAME", pattern: /__PROJECT_NAME__/g },
213+
{ variable: "PROJECT_NAME", pattern: /____PROJECT_NAME____/g },
214214
],
215-
contentReplacers: [{ variable: "AUTHOR", pattern: /__AUTHOR__/g }],
215+
contentReplacers: [{ variable: "AUTHOR", pattern: /____AUTHOR____/g }],
216216
projectQuestions: null,
217217
};
218218

@@ -272,7 +272,7 @@ module.exports = {
272272

273273
const extractedVariables: ExtractedVariables = {
274274
fileReplacers: [
275-
{ variable: "PROJECT_NAME", pattern: /__PROJECT_NAME__/g },
275+
{ variable: "PROJECT_NAME", pattern: /____PROJECT_NAME____/g },
276276
],
277277
contentReplacers: [],
278278
projectQuestions: null,
@@ -289,7 +289,7 @@ module.exports = {
289289
it("should replace variables in file contents", async () => {
290290
fs.writeFileSync(
291291
path.join(testTempDir, "README.md"),
292-
"# __PROJECT_NAME__\n\nBy __AUTHOR__"
292+
"# ____PROJECT_NAME____\n\nBy ____AUTHOR____"
293293
);
294294

295295
const extractedVariables = await extractVariables(testTempDir);
@@ -315,7 +315,7 @@ module.exports = {
315315

316316
it("should replace variables in filenames", async () => {
317317
fs.writeFileSync(
318-
path.join(testTempDir, "__PROJECT_NAME__.config.js"),
318+
path.join(testTempDir, "____PROJECT_NAME____.config.js"),
319319
"module.exports = {};"
320320
);
321321

@@ -338,11 +338,11 @@ module.exports = {
338338
});
339339

340340
it("should replace variables in nested directory names", async () => {
341-
const nestedDir = path.join(testTempDir, "src", "__MODULE_NAME__");
341+
const nestedDir = path.join(testTempDir, "src", "____MODULE_NAME____");
342342
fs.mkdirSync(nestedDir, { recursive: true });
343343
fs.writeFileSync(
344344
path.join(nestedDir, "index.ts"),
345-
'export const name = "__MODULE_NAME__";'
345+
'export const name = "____MODULE_NAME____";'
346346
);
347347

348348
const extractedVariables = await extractVariables(testTempDir);
@@ -389,7 +389,7 @@ module.exports = {
389389
it("should handle multiple occurrences of the same variable", async () => {
390390
fs.writeFileSync(
391391
path.join(testTempDir, "test.txt"),
392-
"__NAME__ loves __NAME__ and __NAME__ is great!"
392+
"____NAME____ loves ____NAME____ and ____NAME____ is great!"
393393
);
394394

395395
const extractedVariables = await extractVariables(testTempDir);
@@ -450,7 +450,7 @@ module.exports = {
450450
fs.mkdirSync(ignoredDir);
451451
fs.writeFileSync(
452452
path.join(ignoredDir, "example.txt"),
453-
"This file has __IGNORED__ variable"
453+
"This file has ____IGNORED____ variable"
454454
);
455455
fs.writeFileSync(
456456
path.join(testTempDir, ".questions.json"),

packages/create-gen-app/src/extract.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import {
88
Questions,
99
} from "./types";
1010

11+
const PLACEHOLDER_BOUNDARY = "____";
12+
1113
/**
12-
* Pattern to match __VARIABLE__ in filenames and content
14+
* Pattern to match ____VARIABLE____ in filenames and content
1315
*/
14-
const VARIABLE_PATTERN = /__([A-Za-z_][A-Za-z0-9_]*)__/g;
16+
const VARIABLE_PATTERN = new RegExp(
17+
`${PLACEHOLDER_BOUNDARY}([A-Za-z_][A-Za-z0-9_]*)${PLACEHOLDER_BOUNDARY}`,
18+
"g"
19+
);
1520

1621
/**
1722
* Extract all variables from a template directory
@@ -27,11 +32,12 @@ export async function extractVariables(
2732
const contentReplacerVars = new Set<string>();
2833

2934
const projectQuestions = await loadProjectQuestions(templateDir);
30-
const ignoredPatterns = new Set<string>(projectQuestions?.ignore ?? []);
35+
const ignoredPathPatterns = new Set<string>(projectQuestions?.ignore ?? []);
36+
const ignoredContentTokens = buildIgnoredContentTokens(projectQuestions);
3137

3238
await walkDirectory(templateDir, async (filePath) => {
3339
const relativePath = path.relative(templateDir, filePath);
34-
if (shouldIgnore(relativePath, ignoredPatterns)) {
40+
if (shouldIgnore(relativePath, ignoredPathPatterns)) {
3541
return;
3642
}
3743

@@ -49,18 +55,27 @@ export async function extractVariables(
4955
fileReplacerVars.add(varName);
5056
fileReplacers.push({
5157
variable: varName,
52-
pattern: new RegExp(`__${varName}__`, "g"),
58+
pattern: new RegExp(
59+
`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
60+
"g"
61+
),
5362
});
5463
}
5564
}
5665

57-
const contentVars = await extractFromFileContent(filePath, ignoredPatterns);
66+
const contentVars = await extractFromFileContent(
67+
filePath,
68+
ignoredContentTokens
69+
);
5870
for (const varName of contentVars) {
5971
if (!contentReplacerVars.has(varName)) {
6072
contentReplacerVars.add(varName);
6173
contentReplacers.push({
6274
variable: varName,
63-
pattern: new RegExp(`__${varName}__`, "g"),
75+
pattern: new RegExp(
76+
`${PLACEHOLDER_BOUNDARY}${varName}${PLACEHOLDER_BOUNDARY}`,
77+
"g"
78+
),
6479
});
6580
}
6681
}
@@ -80,7 +95,7 @@ export async function extractVariables(
8095
*/
8196
async function extractFromFileContent(
8297
filePath: string,
83-
ignoredPatterns: Set<string>
98+
ignoredTokens: Set<string>
8499
): Promise<Set<string>> {
85100
const variables = new Set<string>();
86101

@@ -97,7 +112,7 @@ async function extractFromFileContent(
97112
for (const line of lines) {
98113
const matches = line.matchAll(VARIABLE_PATTERN);
99114
for (const match of matches) {
100-
if (!shouldIgnoreContent(match[0], ignoredPatterns)) {
115+
if (!shouldIgnoreContent(match[0], ignoredTokens)) {
101116
variables.add(match[1]);
102117
}
103118
}
@@ -107,7 +122,7 @@ async function extractFromFileContent(
107122
stream.on("end", () => {
108123
const matches = buffer.matchAll(VARIABLE_PATTERN);
109124
for (const match of matches) {
110-
if (!shouldIgnoreContent(match[0], ignoredPatterns)) {
125+
if (!shouldIgnoreContent(match[0], ignoredTokens)) {
111126
variables.add(match[1]);
112127
}
113128
}
@@ -226,22 +241,49 @@ function shouldIgnore(
226241
return false;
227242
}
228243

229-
const DEFAULT_IGNORED_CONTENT = new Set(["__tests__", "__snapshots__"]);
244+
const DEFAULT_IGNORED_CONTENT_TOKENS = ["tests", "snapshots"];
230245

231246
function shouldIgnoreContent(
232247
match: string,
233-
ignoredPatterns: Set<string>
248+
ignoredTokens: Set<string>
234249
): boolean {
235-
if (ignoredPatterns.size === 0 && DEFAULT_IGNORED_CONTENT.size === 0) {
250+
if (ignoredTokens.size === 0) {
236251
return false;
237252
}
238-
const normalizedMatch = match.slice(2, -2);
239-
return (
240-
ignoredPatterns.has(match) ||
241-
ignoredPatterns.has(normalizedMatch) ||
242-
DEFAULT_IGNORED_CONTENT.has(match) ||
243-
DEFAULT_IGNORED_CONTENT.has(normalizedMatch)
253+
const token = match.slice(
254+
PLACEHOLDER_BOUNDARY.length,
255+
-PLACEHOLDER_BOUNDARY.length
244256
);
257+
return ignoredTokens.has(token);
258+
}
259+
260+
function buildIgnoredContentTokens(projectQuestions: Questions | null): Set<string> {
261+
const tokens = new Set<string>(DEFAULT_IGNORED_CONTENT_TOKENS);
262+
if (projectQuestions?.ignore) {
263+
for (const entry of projectQuestions.ignore) {
264+
const normalized = normalizePlaceholder(entry);
265+
if (normalized) {
266+
tokens.add(normalized);
267+
}
268+
}
269+
}
270+
return tokens;
271+
}
272+
273+
function normalizePlaceholder(entry: string): string | null {
274+
if (
275+
entry.startsWith(PLACEHOLDER_BOUNDARY) &&
276+
entry.endsWith(PLACEHOLDER_BOUNDARY)
277+
) {
278+
return entry.slice(
279+
PLACEHOLDER_BOUNDARY.length,
280+
-PLACEHOLDER_BOUNDARY.length
281+
);
282+
}
283+
if (entry.startsWith("__") && entry.endsWith("__")) {
284+
return entry.slice(2, -2);
285+
}
286+
return entry || null;
245287
}
246288

247289
/**

packages/create-gen-app/src/prompt.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { Inquirerer, Question } from 'inquirerer';
1+
import { Inquirerer, Question } from "inquirerer";
22

3-
import { ExtractedVariables } from './types';
3+
import { ExtractedVariables } from "./types";
4+
5+
const PLACEHOLDER_BOUNDARY = "____";
46

57
/**
68
* Generate questions from extracted variables
79
* @param extractedVariables - Variables extracted from the template
810
* @returns Array of questions to prompt the user
911
*/
10-
export function generateQuestions(extractedVariables: ExtractedVariables): Question[] {
12+
export function generateQuestions(
13+
extractedVariables: ExtractedVariables
14+
): Question[] {
1115
const questions: Question[] = [];
1216
const askedVariables = new Set<string>();
1317

@@ -48,7 +52,16 @@ export function generateQuestions(extractedVariables: ExtractedVariables): Quest
4852
}
4953

5054
function normalizeQuestionName(name: string): string {
51-
if (/^__.+__$/.test(name)) {
55+
if (
56+
name.startsWith(PLACEHOLDER_BOUNDARY) &&
57+
name.endsWith(PLACEHOLDER_BOUNDARY)
58+
) {
59+
return name.slice(
60+
PLACEHOLDER_BOUNDARY.length,
61+
-PLACEHOLDER_BOUNDARY.length
62+
);
63+
}
64+
if (name.startsWith("__") && name.endsWith("__")) {
5265
return name.slice(2, -2);
5366
}
5467
return name;

packages/create-gen-app/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ export interface Questions {
1111
}
1212

1313
/**
14-
* Variable extracted from filename patterns like __VARIABLE__
14+
* Variable extracted from filename patterns like ____VARIABLE____
1515
*/
1616
export interface FileReplacer {
1717
variable: string;
1818
pattern: RegExp;
1919
}
2020

2121
/**
22-
* Variable extracted from file content patterns like __VARIABLE__
22+
* Variable extracted from file content patterns like ____VARIABLE____
2323
*/
2424
export interface ContentReplacer {
2525
variable: string;

0 commit comments

Comments
 (0)