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

Commit 4c9b272

Browse files
committed
feat(create-gen-app): auto-generate LICENSE files from templates
- Add built-in license templates (MIT, Apache-2.0, ISC) in `src/licenses.ts` - Automatically generate or overwrite LICENSE file based on user's selection - Replace placeholders (year, author name, email) with user-provided values - Show friendly warning when template doesn't support selected license type - Update `replaceVariables` to call `ensureLicenseFile()` after file replacement - Add unit tests for license template rendering - Update integration tests to validate LICENSE file generation in real GitHub flows - Document license generation feature in README This ensures generated projects always have proper LICENSE files matching the user's selection, whether the template originally included one or not.
1 parent a269bae commit 4c9b272

File tree

7 files changed

+198
-1
lines changed

7 files changed

+198
-1
lines changed

packages/create-gen-app/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ A TypeScript library for cloning and customizing template repositories with vari
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
26+
- Auto-generate `LICENSE` file content (MIT, Apache-2.0, ISC) based on user answers
2627

2728
## Installation
2829

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ describe("CLI integration (GitHub templates)", () => {
5252
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
5353
expect(pkg.name).toBe(answers.PACKAGE_IDENTIFIER);
5454
expect(pkg.license).toBe(answers.LICENSE);
55+
56+
const licenseContent = fs.readFileSync(
57+
path.join(workspace.outputDir, "LICENSE"),
58+
"utf8"
59+
);
60+
expect(licenseContent).toContain("MIT License");
61+
expect(licenseContent).toContain(answers.USERFULLNAME);
5562
} finally {
5663
cleanupWorkspace(workspace);
5764
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ describe("createGen integration (GitHub templates)", () => {
4343
".questions.json"
4444
);
4545
expect(fs.existsSync(questionsJsonPath)).toBe(false);
46+
47+
const licensePath = path.join(workspace.outputDir, "LICENSE");
48+
expect(fs.existsSync(licensePath)).toBe(true);
49+
const licenseContent = fs.readFileSync(licensePath, "utf8");
50+
expect(licenseContent).toContain(answers.USERFULLNAME);
51+
expect(licenseContent).toContain("MIT License");
4652
} finally {
4753
cleanupWorkspace(workspace);
4854
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { renderLicense, isSupportedLicense, listSupportedLicenses } from "../src/licenses";
2+
3+
describe("license templates", () => {
4+
it("renders MIT license with author and email", () => {
5+
const content = renderLicense("MIT", {
6+
author: "Test User",
7+
email: "test@example.com",
8+
year: "2099",
9+
});
10+
expect(content).toContain("Test User");
11+
expect(content).toContain("<test@example.com>");
12+
expect(content).toContain("2099");
13+
});
14+
15+
it("falls back when license not supported", () => {
16+
expect(renderLicense("UNKNOWN", {})).toBeNull();
17+
expect(isSupportedLicense("UNKNOWN")).toBe(false);
18+
});
19+
20+
it("lists supported licenses", () => {
21+
const supported = listSupportedLicenses();
22+
expect(supported).toEqual(expect.arrayContaining(["MIT", "APACHE-2.0", "ISC"]));
23+
});
24+
});
25+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const PLACEHOLDER_PATTERN = /{{(\w+)}}/g;
2+
3+
interface LicenseContext {
4+
year: string;
5+
author: string;
6+
email: string;
7+
}
8+
9+
const LICENSE_TEMPLATES: Record<string, string> = {
10+
MIT: `The MIT License (MIT)
11+
12+
Copyright (c) {{YEAR}} {{AUTHOR}}{{EMAIL_LINE}}
13+
14+
Permission is hereby granted, free of charge, to any person obtaining a copy
15+
of this software and associated documentation files (the "Software"), to deal
16+
in the Software without restriction, including without limitation the rights
17+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18+
copies of the Software, and to permit persons to whom the Software is
19+
furnished to do so, subject to the following conditions:
20+
21+
The above copyright notice and this permission notice shall be included in all
22+
copies or substantial portions of the Software.
23+
24+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30+
SOFTWARE.
31+
`,
32+
"APACHE-2.0": `Apache License
33+
Version 2.0, January 2004
34+
http://www.apache.org/licenses/
35+
36+
Copyright (c) {{YEAR}} {{AUTHOR}}{{EMAIL_LINE}}
37+
38+
Licensed under the Apache License, Version 2.0 (the "License");
39+
you may not use this file except in compliance with the License.
40+
You may obtain a copy of the License at
41+
42+
http://www.apache.org/licenses/LICENSE-2.0
43+
44+
Unless required by applicable law or agreed to in writing, software
45+
distributed under the License is distributed on an "AS IS" BASIS,
46+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
47+
See the License for the specific language governing permissions and
48+
limitations under the License.
49+
`,
50+
ISC: `ISC License
51+
52+
Copyright (c) {{YEAR}} {{AUTHOR}}{{EMAIL_LINE}}
53+
54+
Permission to use, copy, modify, and/or distribute this software for any
55+
purpose with or without fee is hereby granted, provided that the above
56+
copyright notice and this permission notice appear in all copies.
57+
58+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
59+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
60+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
61+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
62+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
63+
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
64+
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
65+
`,
66+
};
67+
68+
export type SupportedLicense = keyof typeof LICENSE_TEMPLATES;
69+
70+
export function isSupportedLicense(name: string): name is SupportedLicense {
71+
return name.toUpperCase() in LICENSE_TEMPLATES;
72+
}
73+
74+
export function renderLicense(
75+
licenseName: string,
76+
context: Partial<LicenseContext>
77+
): string | null {
78+
if (!licenseName) {
79+
return null;
80+
}
81+
const normalized = licenseName.toUpperCase() as SupportedLicense;
82+
const template = LICENSE_TEMPLATES[normalized];
83+
if (!template) {
84+
return null;
85+
}
86+
87+
const ctx: LicenseContext = {
88+
year: context.year ?? new Date().getFullYear().toString(),
89+
author: context.author ?? "Unknown Author",
90+
email: context.email ?? "",
91+
};
92+
93+
const emailLine = ctx.email ? ` <${ctx.email}>` : "";
94+
95+
return template.replace(PLACEHOLDER_PATTERN, (_, rawKey: string) => {
96+
const key = rawKey.toUpperCase();
97+
if (key === "EMAIL_LINE") {
98+
return emailLine;
99+
}
100+
const normalizedKey = key.toLowerCase() as keyof LicenseContext;
101+
const value = ctx[normalizedKey];
102+
return value || "";
103+
});
104+
}
105+
106+
export function listSupportedLicenses(): string[] {
107+
return Object.keys(LICENSE_TEMPLATES);
108+
}
109+

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Transform } from 'stream';
44
import { pipeline } from 'stream/promises';
55

66
import { ExtractedVariables } from './types';
7+
import { renderLicense, isSupportedLicense } from './licenses';
78

89
/**
910
* Replace variables in all files in the template directory
@@ -23,6 +24,7 @@ export async function replaceVariables(
2324
}
2425

2526
await walkAndReplace(templateDir, outputDir, extractedVariables, answers);
27+
await ensureLicenseFile(outputDir, answers);
2628
}
2729

2830
/**
@@ -79,6 +81,49 @@ async function walkAndReplace(
7981
}
8082
}
8183

84+
async function ensureLicenseFile(
85+
outputDir: string,
86+
answers: Record<string, any>
87+
): Promise<void> {
88+
const licenseValue = answers?.LICENSE;
89+
if (typeof licenseValue !== 'string' || licenseValue.trim() === '') {
90+
return;
91+
}
92+
93+
const selectedLicense = licenseValue.trim();
94+
if (!isSupportedLicense(selectedLicense)) {
95+
console.warn(
96+
`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`
97+
);
98+
return;
99+
}
100+
101+
const author =
102+
answers?.USERFULLNAME ??
103+
answers?.AUTHOR ??
104+
answers?.AUTHORFULLNAME ??
105+
answers?.USERNAME ??
106+
'Unknown Author';
107+
108+
const email = answers?.USEREMAIL ?? answers?.EMAIL ?? '';
109+
110+
const content = renderLicense(selectedLicense, {
111+
author: String(author),
112+
email: String(email || ''),
113+
});
114+
115+
if (!content) {
116+
return;
117+
}
118+
119+
const licensePath = path.join(outputDir, 'LICENSE');
120+
fs.mkdirSync(path.dirname(licensePath), { recursive: true });
121+
fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
122+
console.log(
123+
`[create-gen-app] LICENSE updated with ${selectedLicense} template.`
124+
);
125+
}
126+
82127
/**
83128
* Replace variables in a file using streams
84129
* @param sourcePath - Source file path

packages/create-gen-app/test-utils/integration-helpers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ export function cleanupWorkspace(workspace: TempWorkspace): void {
2525
fs.rmSync(workspace.baseDir, { recursive: true, force: true });
2626
}
2727

28-
export function buildAnswers(suffix: string): Record<string, string> {
28+
export function buildAnswers(
29+
suffix: string,
30+
overrides: Partial<Record<string, string>> = {}
31+
): Record<string, string> {
2932
const safeSuffix = suffix.replace(/[^a-z0-9]/gi, "-").toLowerCase();
3033
return {
3134
USERFULLNAME: `Test User ${suffix}`,
@@ -37,6 +40,7 @@ export function buildAnswers(suffix: string): Record<string, string> {
3740
ACCESS: "public",
3841
LICENSE: "MIT",
3942
PACKAGE_IDENTIFIER: `integration-${safeSuffix}`,
43+
...overrides,
4044
};
4145
}
4246

0 commit comments

Comments
 (0)