Skip to content

Commit b88ec2a

Browse files
committed
Add create-net npx script for project scaffolding
Implements a Node.js CLI tool that can be used with npx to create new projects from GitHub template repositories. The script: - Downloads GitHub repository archives from NetCoreTemplates or custom organizations - Extracts archives into a project folder with the specified name - Replaces all variations of "MyApp" with the project name in files and folders - Supports multiple naming conventions (kebab-case, snake_case, PascalCase, etc.) - Automatically runs npm install in directories containing package.json Usage: npx create-net <repo> <ProjectName> npx create-net nextjs MyProject npx create-net NetFrameworkTemplates/web-netfx MyProject
0 parents  commit b88ec2a

File tree

5 files changed

+401
-0
lines changed

5 files changed

+401
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
*.log
3+
.DS_Store
4+
temp-download.zip
5+
temp-extract/

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# create-net
2+
3+
Create .NET and other projects from NetCoreTemplates GitHub repositories.
4+
5+
## Usage
6+
7+
```bash
8+
npx create-net <repo> <ProjectName>
9+
```
10+
11+
### Examples
12+
13+
**Create a project from NetCoreTemplates organization:**
14+
15+
```bash
16+
npx create-net nextjs MyProject
17+
```
18+
19+
This downloads from: `https://github.com/NetCoreTemplates/nextjs`
20+
21+
**Create a project from a different organization:**
22+
23+
```bash
24+
npx create-net NetFrameworkTemplates/web-netfx MyProject
25+
```
26+
27+
This downloads from: `https://github.com/NetFrameworkTemplates/web-netfx`
28+
29+
## What it does
30+
31+
1. **Downloads** the GitHub repository archive from the specified repository
32+
2. **Extracts** the archive into a folder named `<ProjectName>`
33+
3. **Replaces** all variations of `MyApp` with variations of your `<ProjectName>`:
34+
- `My_App``Your_Project`
35+
- `My App``Your Project`
36+
- `my-app``your-project`
37+
- `my_app``your_project`
38+
- `myapp``yourproject`
39+
- `my.app``your.project`
40+
- `MyApp``YourProject`
41+
4. **Renames** files and folders containing `MyApp` variations
42+
5. **Runs** `npm install` in all directories containing `package.json`
43+
44+
## Requirements
45+
46+
- Node.js >= 14.0.0
47+
48+
## Publishing
49+
50+
To publish this package to npm:
51+
52+
```bash
53+
npm publish
54+
```
55+
56+
## License
57+
58+
MIT

bin/create-net.js

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
#!/usr/bin/env node
2+
3+
const https = require('https');
4+
const fs = require('fs');
5+
const path = require('path');
6+
const { execSync } = require('child_process');
7+
const AdmZip = require('adm-zip');
8+
9+
// Parse command line arguments
10+
const args = process.argv.slice(2);
11+
12+
if (args.length < 2) {
13+
console.error('Usage: npx create-net <repo> <ProjectName>');
14+
console.error('Example: npx create-net nextjs MyProject');
15+
console.error('Example: npx create-net NetFrameworkTemplates/web-netfx MyProject');
16+
process.exit(1);
17+
}
18+
19+
const [repo, projectName] = args;
20+
21+
// Determine organization and repository
22+
let organization = 'NetCoreTemplates';
23+
let repository = repo;
24+
25+
if (repo.includes('/')) {
26+
const parts = repo.split('/');
27+
organization = parts[0];
28+
repository = parts[1];
29+
}
30+
31+
// Construct GitHub archive URL
32+
const archiveUrl = `https://github.com/${organization}/${repository}/archive/refs/heads/main.zip`;
33+
const tempZipPath = path.join(process.cwd(), 'temp-download.zip');
34+
const projectPath = path.join(process.cwd(), projectName);
35+
36+
console.log(`Creating project "${projectName}" from ${organization}/${repository}...`);
37+
console.log(`Downloading from: ${archiveUrl}`);
38+
39+
// Check if project directory already exists
40+
if (fs.existsSync(projectPath)) {
41+
console.error(`Error: Directory "${projectName}" already exists.`);
42+
process.exit(1);
43+
}
44+
45+
// Function to download file from URL
46+
function downloadFile(url, destination) {
47+
return new Promise((resolve, reject) => {
48+
const file = fs.createWriteStream(destination);
49+
50+
https.get(url, (response) => {
51+
// Handle redirects
52+
if (response.statusCode === 301 || response.statusCode === 302) {
53+
file.close();
54+
fs.unlinkSync(destination);
55+
return downloadFile(response.headers.location, destination)
56+
.then(resolve)
57+
.catch(reject);
58+
}
59+
60+
if (response.statusCode !== 200) {
61+
file.close();
62+
fs.unlinkSync(destination);
63+
return reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
64+
}
65+
66+
response.pipe(file);
67+
68+
file.on('finish', () => {
69+
file.close();
70+
resolve();
71+
});
72+
}).on('error', (err) => {
73+
file.close();
74+
fs.unlinkSync(destination);
75+
reject(err);
76+
});
77+
});
78+
}
79+
80+
// Function to convert string to different case variations
81+
function getNameVariations(name) {
82+
// Convert ProjectName to different formats
83+
84+
// Split by capital letters to get words
85+
const words = name.split(/(?=[A-Z])/).filter(w => w.length > 0);
86+
87+
return {
88+
// my_app
89+
snake_case: words.map(w => w.toLowerCase()).join('_'),
90+
// my-app
91+
kebab_case: words.map(w => w.toLowerCase()).join('-'),
92+
// myapp
93+
lowercase: words.map(w => w.toLowerCase()).join(''),
94+
// my.app
95+
dot_case: words.map(w => w.toLowerCase()).join('.'),
96+
// My_App
97+
pascal_snake: words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('_'),
98+
// My App
99+
title_space: words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '),
100+
// MyApp (PascalCase)
101+
pascal_case: words.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('')
102+
};
103+
}
104+
105+
// Function to replace content in text files
106+
function replaceInFile(filePath, replacements) {
107+
// Skip binary files
108+
const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.zip', '.exe', '.dll', '.so', '.dylib', '.pdf', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.webm', '.ogg', '.mp3', '.wav'];
109+
const ext = path.extname(filePath).toLowerCase();
110+
111+
if (binaryExtensions.includes(ext)) {
112+
return;
113+
}
114+
115+
try {
116+
let content = fs.readFileSync(filePath, 'utf8');
117+
let modified = false;
118+
119+
for (const [oldStr, newStr] of Object.entries(replacements)) {
120+
if (content.includes(oldStr)) {
121+
content = content.split(oldStr).join(newStr);
122+
modified = true;
123+
}
124+
}
125+
126+
if (modified) {
127+
fs.writeFileSync(filePath, content, 'utf8');
128+
}
129+
} catch (err) {
130+
// If file is binary or can't be read as text, skip it
131+
if (err.code !== 'ENOENT') {
132+
// Silently skip files that can't be processed
133+
}
134+
}
135+
}
136+
137+
// Function to rename files and directories
138+
function renamePathsRecursive(dirPath, replacements) {
139+
const items = fs.readdirSync(dirPath);
140+
141+
// Process files and directories
142+
for (const item of items) {
143+
const oldPath = path.join(dirPath, item);
144+
const stats = fs.statSync(oldPath);
145+
146+
if (stats.isDirectory()) {
147+
// Recursively process directory first
148+
renamePathsRecursive(oldPath, replacements);
149+
} else {
150+
// Replace content in files
151+
replaceInFile(oldPath, replacements);
152+
}
153+
}
154+
155+
// Rename files and directories in this directory
156+
for (const item of items) {
157+
const oldPath = path.join(dirPath, item);
158+
let newName = item;
159+
160+
// Apply replacements to the name
161+
for (const [oldStr, newStr] of Object.entries(replacements)) {
162+
if (newName.includes(oldStr)) {
163+
newName = newName.split(oldStr).join(newStr);
164+
}
165+
}
166+
167+
if (newName !== item) {
168+
const newPath = path.join(dirPath, newName);
169+
fs.renameSync(oldPath, newPath);
170+
console.log(`Renamed: ${item} -> ${newName}`);
171+
}
172+
}
173+
}
174+
175+
// Function to find and run npm install in directories with package.json
176+
function runNpmInstall(dirPath) {
177+
const items = fs.readdirSync(dirPath);
178+
179+
// Check if current directory has package.json
180+
if (items.includes('package.json')) {
181+
console.log(`Running npm install in ${dirPath}...`);
182+
try {
183+
execSync('npm install', {
184+
cwd: dirPath,
185+
stdio: 'inherit'
186+
});
187+
} catch (err) {
188+
console.error(`Warning: npm install failed in ${dirPath}`);
189+
}
190+
}
191+
192+
// Recursively check subdirectories
193+
for (const item of items) {
194+
const itemPath = path.join(dirPath, item);
195+
const stats = fs.statSync(itemPath);
196+
197+
if (stats.isDirectory() && item !== 'node_modules' && item !== '.git') {
198+
runNpmInstall(itemPath);
199+
}
200+
}
201+
}
202+
203+
// Main execution
204+
async function main() {
205+
try {
206+
// Download the archive
207+
console.log('Downloading archive...');
208+
await downloadFile(archiveUrl, tempZipPath);
209+
console.log('Download complete!');
210+
211+
// Extract the archive
212+
console.log('Extracting archive...');
213+
const zip = new AdmZip(tempZipPath);
214+
const zipEntries = zip.getEntries();
215+
216+
// Get the root folder name from the zip (usually repo-main)
217+
const rootFolder = zipEntries[0].entryName.split('/')[0];
218+
219+
// Extract to temporary location
220+
const tempExtractPath = path.join(process.cwd(), 'temp-extract');
221+
zip.extractAllTo(tempExtractPath, true);
222+
223+
// Move the extracted folder to the project name
224+
const extractedPath = path.join(tempExtractPath, rootFolder);
225+
fs.renameSync(extractedPath, projectPath);
226+
227+
// Clean up temp extract directory
228+
fs.rmdirSync(tempExtractPath, { recursive: true });
229+
230+
// Clean up temp zip file
231+
fs.unlinkSync(tempZipPath);
232+
233+
console.log('Extraction complete!');
234+
235+
// Prepare replacements
236+
console.log('Replacing template names with project name...');
237+
238+
const sourceVariations = getNameVariations('MyApp');
239+
const targetVariations = getNameVariations(projectName);
240+
241+
const replacements = {
242+
[sourceVariations.pascal_snake]: targetVariations.pascal_snake, // My_App => Acme_Corp
243+
[sourceVariations.title_space]: targetVariations.title_space, // My App => Acme Corp
244+
[sourceVariations.kebab_case]: targetVariations.kebab_case, // my-app => acme-corp
245+
[sourceVariations.snake_case]: targetVariations.snake_case, // my_app => acme_corp
246+
[sourceVariations.lowercase]: targetVariations.lowercase, // myapp => acmecorp
247+
[sourceVariations.dot_case]: targetVariations.dot_case, // my.app => acme.corp
248+
[sourceVariations.pascal_case]: targetVariations.pascal_case // MyApp => AcmeCorp
249+
};
250+
251+
// Apply replacements to all files and rename paths
252+
renamePathsRecursive(projectPath, replacements);
253+
254+
console.log('Template name replacement complete!');
255+
256+
// Run npm install in all directories with package.json
257+
console.log('Installing dependencies...');
258+
runNpmInstall(projectPath);
259+
260+
console.log('\n✓ Project created successfully!');
261+
console.log(`\nNext steps:`);
262+
console.log(` cd ${projectName}`);
263+
console.log(` npm start (or appropriate command for your template)`);
264+
265+
} catch (err) {
266+
console.error('Error creating project:', err.message);
267+
268+
// Clean up on error
269+
if (fs.existsSync(tempZipPath)) {
270+
fs.unlinkSync(tempZipPath);
271+
}
272+
if (fs.existsSync(projectPath)) {
273+
fs.rmSync(projectPath, { recursive: true, force: true });
274+
}
275+
276+
process.exit(1);
277+
}
278+
}
279+
280+
main();

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)