Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

exports[`cached template integration tests first clone with variable replacement should snapshot created directory structure 1`] = `
[
".boilerplate.json",
"LICENSE",
"README.md",
"__tests__/",
Expand Down Expand Up @@ -39,7 +38,7 @@ exports[`cached template integration tests first clone with variable replacement
"scripts": {
"lint": "eslint . --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:watch": "jest --watchAll",
},
"version": "0.0.1",
},
Expand All @@ -48,7 +47,6 @@ exports[`cached template integration tests first clone with variable replacement

exports[`cached template integration tests second clone from cache should snapshot created directory structure from cache 1`] = `
[
".boilerplate.json",
"LICENSE",
"README.md",
"__tests__/",
Expand Down Expand Up @@ -85,7 +83,7 @@ exports[`cached template integration tests second clone from cache should snapsh
"scripts": {
"lint": "eslint . --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:watch": "jest --watchAll",
},
"version": "0.0.1",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CacheManager, GitCloner } from 'create-gen-app';

export const TEST_REPO =
process.env.CREATE_GEN_TEST_REPO ??
'https://github.com/constructive-io/pgpm-boilerplates.git';
'https://github.com/constructive-io/pgpm-boilerplates-testing.git';
export const TEST_BRANCH =
process.env.CREATE_GEN_TEST_BRANCH ?? 'main';
export const TEST_TEMPLATE_DIR = (() => {
Expand Down
7 changes: 5 additions & 2 deletions packages/create-gen-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,11 @@ export const author = "____fullName____";

### Custom Questions

Create a `.questions.json`:
Create a `.boilerplate.json`:

```json
{
"type": "module",
"questions": [
{
"name": "____fullName____",
Expand All @@ -135,7 +136,9 @@ Create a `.questions.json`:
}
```

Or `.questions.js` for dynamic logic. Question names can use `____var____` or plain `VAR`; they'll be normalized automatically.
Or `.boilerplate.js` for dynamic logic. Question names can use `____var____` or plain `VAR`; they'll be normalized automatically.

Note: `.boilerplate.json`, `.boilerplate.js`, and `.boilerplates.json` files are automatically excluded from the generated output.

### License Templates

Expand Down
37 changes: 23 additions & 14 deletions packages/create-gen-app/__tests__/create-gen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ describe('create-gen-app', () => {
);
});

it('should load questions from .questions.json', async () => {
const questions = {
it('should load questions from .boilerplate.json', async () => {
const boilerplate = {
type: 'module',
questions: [
{
name: 'projectName',
Expand All @@ -111,8 +112,8 @@ describe('create-gen-app', () => {
};

fs.writeFileSync(
path.join(testTempDir, '.questions.json'),
JSON.stringify(questions, null, 2)
path.join(testTempDir, '.boilerplate.json'),
JSON.stringify(boilerplate, null, 2)
);

const result = await extractVariables(testTempDir);
Expand All @@ -122,9 +123,10 @@ describe('create-gen-app', () => {
expect(result.projectQuestions?.questions[0].name).toBe('projectName');
});

it('should load questions from .questions.js', async () => {
const questionsContent = `
it('should load questions from .boilerplate.js', async () => {
const boilerplateContent = `
module.exports = {
type: 'module',
questions: [
{
name: 'projectName',
Expand All @@ -136,8 +138,8 @@ module.exports = {
`;

fs.writeFileSync(
path.join(testTempDir, '.questions.js'),
questionsContent
path.join(testTempDir, '.boilerplate.js'),
boilerplateContent
);

const result = await extractVariables(testTempDir);
Expand All @@ -157,9 +159,9 @@ module.exports = {
expect(result.projectQuestions).toBeNull();
});

it('should skip .questions.json and .questions.js from variable extraction', async () => {
it('should skip .boilerplate.json and .boilerplate.js from variable extraction', async () => {
fs.writeFileSync(
path.join(testTempDir, '.questions.json'),
path.join(testTempDir, '.boilerplate.json'),
'{"questions": [{"name": "____shouldNotExtract____"}]}'
);

Expand Down Expand Up @@ -486,10 +488,14 @@ module.exports = {
expect(content).toBe('export const name = "auth";');
});

it('should skip .questions.json and .questions.js files', async () => {
it('should skip .boilerplate.json and .boilerplate.js files', async () => {
fs.writeFileSync(
path.join(testTempDir, '.questions.json'),
'{"questions": []}'
path.join(testTempDir, '.boilerplate.json'),
'{"type": "module", "questions": []}'
);
fs.writeFileSync(
path.join(testTempDir, '.boilerplates.json'),
'{"dir": "default"}'
);
fs.writeFileSync(path.join(testTempDir, 'README.md'), 'Regular file');

Expand All @@ -501,7 +507,10 @@ module.exports = {
{}
);

expect(fs.existsSync(path.join(testOutputDir, '.questions.json'))).toBe(
expect(fs.existsSync(path.join(testOutputDir, '.boilerplate.json'))).toBe(
false
);
expect(fs.existsSync(path.join(testOutputDir, '.boilerplates.json'))).toBe(
false
);
expect(fs.existsSync(path.join(testOutputDir, 'README.md'))).toBe(true);
Expand Down
24 changes: 15 additions & 9 deletions packages/create-gen-app/src/template/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ export async function extractVariables(
await walkDirectory(templateDir, async (filePath) => {
const relativePath = path.relative(templateDir, filePath);

// Skip boilerplate configuration files from variable extraction
if (
relativePath === '.questions.json' ||
relativePath === '.questions.js'
relativePath === '.boilerplate.json' ||
relativePath === '.boilerplate.js' ||
relativePath === '.boilerplates.json'
) {
return;
}
Expand Down Expand Up @@ -144,36 +146,40 @@ async function walkDirectory(
}

/**
* Load project questions from .questions.json or .questions.js
* Load project questions from .boilerplate.json or .boilerplate.js
* @param templateDir - Path to the template directory
* @returns Questions object or null if not found
*/
async function loadProjectQuestions(
templateDir: string
): Promise<Questions | null> {
const jsonPath = path.join(templateDir, '.questions.json');
const jsonPath = path.join(templateDir, '.boilerplate.json');
if (fs.existsSync(jsonPath)) {
try {
const content = fs.readFileSync(jsonPath, 'utf8');
const questions = JSON.parse(content);
const parsed = JSON.parse(content);
// .boilerplate.json has { questions: [...] } structure
const questions = parsed.questions ? { questions: parsed.questions } : parsed;
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.warn(`Failed to parse .questions.json: ${errorMessage}`);
console.warn(`Failed to parse .boilerplate.json: ${errorMessage}`);
}
}

const jsPath = path.join(templateDir, '.questions.js');
const jsPath = path.join(templateDir, '.boilerplate.js');
if (fs.existsSync(jsPath)) {
try {
const module = require(jsPath);
const questions = module.default || module;
const exported = module.default || module;
// .boilerplate.js can export { questions: [...] } or just the questions array
const questions = exported.questions ? { questions: exported.questions } : exported;
return validateQuestions(questions) ? normalizeQuestions(questions) : null;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.warn(`Failed to load .questions.js: ${errorMessage}`);
console.warn(`Failed to load .boilerplate.js: ${errorMessage}`);
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/create-gen-app/src/template/replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ async function walkAndReplace(
for (const entry of entries) {
const sourceEntryPath = path.join(currentSource, entry.name);

if (entry.name === '.questions.json' || entry.name === '.questions.js') {
// Skip template configuration files - they should not be copied to output
if (
entry.name === '.boilerplate.json' ||
entry.name === '.boilerplate.js' ||
entry.name === '.boilerplates.json'
) {
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/create-gen-app/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Question } from 'inquirerer';

/**
* Questions configuration that can be loaded from .questions.json or .questions.js
* Questions configuration that can be loaded from .boilerplate.json or .boilerplate.js
* @typedef {Object} Questions
* @property {Question[]} questions - Array of inquirerer questions
*/
Expand Down