├── .gitignore ├── README.md ├── package.json ├── LICENSE ├── src ├── test.js └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate Repo from Template 2 | 3 | Generate builder app scaffolding with examples for various frameworks and SDKs 4 | 5 | https://github.com/user-attachments/assets/92fd670e-65b3-4c73-9133-a3eb6e296749 6 | 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@builder.io/generate-repo-from-template", 3 | "version": "0.0.2", 4 | "bin": { 5 | "generate-repo-from-template": "./src/index.js" 6 | }, 7 | "scripts": { 8 | "test": "node src/test.js" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.6.7", 12 | "inquirer": "^8.2.4", 13 | "fs-extra": "^11.1.1", 14 | "progress": "^2.0.3", 15 | "chalk": "^4.1.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Builder.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs-extra"); 4 | const path = require("path"); 5 | const chalk = require("chalk"); 6 | const { utils, getExampleFolders, generateTemplate } = require('./index.js'); 7 | 8 | const MY_API_KEY = 'ad30f9a246614faaa6a03374f83554c9'; 9 | const TEST_DIR = 'test-results'; 10 | 11 | // Extend the base log with test-specific styling 12 | const log = { 13 | ...utils.log, 14 | info: (...args) => console.log(chalk.blue('ℹ'), ...args), 15 | title: (...args) => console.log(chalk.bold('\n🧪', ...args)), 16 | }; 17 | 18 | async function getAllTemplates() { 19 | const examples = await getExampleFolders(); 20 | 21 | if (!examples || examples.length === 0) { 22 | log.error('No templates found in the examples directory'); 23 | process.exit(1); 24 | } 25 | 26 | return examples; 27 | } 28 | 29 | async function createTestProject(framework, gen, template) { 30 | const projectName = `${framework.toLowerCase().replace('.', '')}-${gen.toLowerCase()}-${template}`; 31 | const projectDir = path.join(process.cwd(), TEST_DIR, projectName); 32 | 33 | try { 34 | await generateTemplate({ 35 | directory: projectDir, 36 | template, 37 | apiKey: MY_API_KEY, 38 | silent: true 39 | }); 40 | return { success: true, path: projectDir }; 41 | } catch (error) { 42 | return { success: false, error: error.message }; 43 | } 44 | } 45 | 46 | async function testPromptChoices() { 47 | const examples = await getAllTemplates(); 48 | const results = { 49 | prompts: [], 50 | errors: [], 51 | projects: [] 52 | }; 53 | 54 | try { 55 | // Test directory name validation 56 | const directoryValidation = { 57 | name: 'Directory Name Validation', 58 | tests: [ 59 | { input: '', expected: false, message: 'should reject empty input' }, 60 | { input: 'valid-name', expected: true, message: 'should accept valid name' }, 61 | { input: 'invalid@name', expected: false, message: 'should reject invalid characters' }, 62 | ] 63 | }; 64 | 65 | const validateDirectory = (input) => { 66 | if (!input.length) return 'Project name is required'; 67 | if (!/^[a-zA-Z0-9-_]+$/.test(input)) return 'Project name can only contain letters, numbers, dashes and underscores'; 68 | return true; 69 | }; 70 | 71 | directoryValidation.tests.forEach(test => { 72 | const result = validateDirectory(test.input); 73 | const passed = (result === true) === test.expected; 74 | if (!passed) { 75 | results.errors.push(`Directory validation failed: ${test.message}`); 76 | } 77 | }); 78 | results.prompts.push(directoryValidation); 79 | 80 | // Test framework choices 81 | const frameworkChoices = ['React', 'Next.js', 'Vue', 'Svelte', 'Angular', 'Nuxt', 'Qwik', 'Remix', 'SolidJS', 'SvelteKit', 'Vue', 'React Native']; 82 | results.prompts.push({ 83 | name: 'Framework Choices', 84 | choices: frameworkChoices 85 | }); 86 | 87 | // Test SDK generation choices 88 | const genChoices = ['Gen1', 'Gen2']; 89 | results.prompts.push({ 90 | name: 'SDK Generation Choices', 91 | choices: genChoices 92 | }); 93 | 94 | // Clean up previous test results 95 | await fs.remove(path.join(process.cwd(), TEST_DIR)); 96 | await fs.ensureDir(path.join(process.cwd(), TEST_DIR)); 97 | 98 | // Test template filtering and create projects 99 | const templateTests = []; 100 | for (const framework of frameworkChoices) { 101 | for (const gen of genChoices) { 102 | try { 103 | const filteredTemplates = examples.filter(example => { 104 | const category = utils.categorizeTemplate(example); 105 | return category?.framework === framework && category?.gen === gen; 106 | }); 107 | 108 | const test = { 109 | framework, 110 | gen, 111 | templatesFound: filteredTemplates.length, 112 | templates: filteredTemplates.map(template => ({ 113 | name: `${template} ${chalk.gray(`(${utils.getTemplateUrl(template)})`)}`, 114 | value: template 115 | })) 116 | }; 117 | 118 | // Create test projects for valid templates 119 | if (filteredTemplates.length > 0) { 120 | for (const template of filteredTemplates) { 121 | const projectResult = await createTestProject(framework, gen, template); 122 | results.projects.push({ 123 | framework, 124 | gen, 125 | template, 126 | ...projectResult 127 | }); 128 | } 129 | } 130 | 131 | templateTests.push(test); 132 | } catch (error) { 133 | results.errors.push(`Failed to filter templates for ${framework} ${gen}: ${error.message}`); 134 | } 135 | } 136 | } 137 | results.prompts.push({ 138 | name: 'Template Filtering', 139 | tests: templateTests 140 | }); 141 | 142 | } catch (error) { 143 | results.errors.push(`Test suite failed: ${error.message}`); 144 | } 145 | 146 | return results; 147 | } 148 | 149 | async function runTests() { 150 | log.title('Running CLI Prompt Tests'); 151 | 152 | const results = await testPromptChoices(); 153 | 154 | log.title('Test Results'); 155 | 156 | // Display prompt test results 157 | results.prompts.forEach(prompt => { 158 | log.info(`\nTesting: ${prompt.name}`); 159 | 160 | if (prompt.tests) { 161 | prompt.tests.forEach(test => { 162 | if (prompt.name === 'Template Filtering') { 163 | const status = test.templatesFound > 0 ? chalk.green('✓') : chalk.yellow('⚠'); 164 | console.log(` ${status} ${test.framework} - ${test.gen}: ${test.templatesFound} templates`); 165 | if (test.templatesFound > 0) { 166 | test.templates.forEach(template => { 167 | console.log(` - ${template.name}`); 168 | }); 169 | } 170 | } else { 171 | console.log(` - ${test.message}`); 172 | } 173 | }); 174 | } else if (prompt.choices) { 175 | console.log(' Available choices:'); 176 | prompt.choices.forEach(choice => { 177 | console.log(` - ${choice}`); 178 | }); 179 | } 180 | }); 181 | 182 | // Display project creation results 183 | if (results.projects.length > 0) { 184 | log.info('\nProject Creation Results:'); 185 | results.projects.forEach(project => { 186 | const status = project.success ? chalk.green('✓') : chalk.red('✖'); 187 | console.log(` ${status} ${project.framework} - ${project.gen} - ${project.template}`); 188 | if (project.success) { 189 | console.log(chalk.gray(` Created at: ${project.path}`)); 190 | } else { 191 | console.log(chalk.red(` Failed: ${project.error}`)); 192 | } 193 | }); 194 | } 195 | 196 | // Display errors if any 197 | if (results.errors.length > 0) { 198 | log.error('\nErrors:'); 199 | results.errors.forEach(error => { 200 | console.log(chalk.red(` - ${error}`)); 201 | }); 202 | } 203 | 204 | // Summary 205 | console.log('\nSummary:'); 206 | console.log(`Total Prompts Tested: ${results.prompts.length}`); 207 | console.log(`Projects Created: ${chalk.cyan(results.projects.filter(p => p.success).length)}`); 208 | console.log(`Projects Failed: ${chalk.red(results.projects.filter(p => !p.success).length)}`); 209 | console.log(`Errors Found: ${chalk.red(results.errors.length)}`); 210 | 211 | // Exit with appropriate code 212 | process.exit(results.errors.length > 0 ? 1 : 0); 213 | } 214 | 215 | runTests().catch(error => { 216 | log.error('Test runner failed:', error.message); 217 | process.exit(1); 218 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const inquirer = require("inquirer"); 4 | const fs = require("fs-extra"); 5 | const path = require("path"); 6 | const axios = require("axios"); 7 | const ProgressBar = require('progress'); 8 | const chalk = require('chalk'); 9 | 10 | const ROOT_EXAMPLES_DIR = "packages/sdks/snippets"; 11 | const GITHUB_RAW_URL = "https://raw.githubusercontent.com/BuilderIO/builder/main"; 12 | const GITHUB_HTML_URL = "https://github.com/BuilderIO/builder/tree/main"; 13 | 14 | const CONSTANTS = { 15 | ROOT_EXAMPLES_DIR, 16 | GITHUB_HTML_URL, 17 | GITHUB_RAW_URL 18 | }; 19 | 20 | const utils = { 21 | categorizeTemplate(templateName) { 22 | const isGen1 = templateName.startsWith('gen1-'); 23 | const gen = isGen1 ? 'Gen1' : 'Gen2'; 24 | 25 | if (isGen1) { 26 | templateName = templateName.replace('gen1-', ''); 27 | } 28 | 29 | if (templateName.startsWith('react-native')) { 30 | return { framework: 'React Native', gen }; 31 | } 32 | if (templateName.startsWith('react-sdk')) { 33 | return { framework: 'Next.js', gen }; 34 | } 35 | 36 | const frameworkPatterns = { 37 | 'angular': { 38 | name: 'Angular', 39 | match: (name) => name.startsWith('angular'), 40 | }, 41 | 'solidjs': { 42 | name: 'SolidJS', 43 | match: (name) => name.startsWith('solidjs'), 44 | }, 45 | 'react': { 46 | name: 'React', 47 | match: (name) => name === 'react' || name.startsWith('react-') && !name.startsWith('react-native') && !name.startsWith('react-sdk'), 48 | }, 49 | 'hydrogen': { 50 | name: 'Hydrogen', 51 | match: (name) => name.startsWith('hydrogen'), 52 | }, 53 | 'next': { 54 | name: 'Next.js', 55 | match: (name) => name.startsWith('next'), 56 | }, 57 | 'vue': { 58 | name: 'Vue', 59 | match: (name) => name === 'vue', 60 | }, 61 | 'nuxt': { 62 | name: 'Nuxt', 63 | match: (name) => name.startsWith('nuxt'), 64 | }, 65 | 'qwik': { 66 | name: 'Qwik', 67 | match: (name) => name.startsWith('qwik'), 68 | }, 69 | 'remix': { 70 | name: 'Remix', 71 | match: (name) => name.startsWith('remix'), 72 | }, 73 | 'svelte': { 74 | name: 'Svelte', 75 | match: (name) => name === 'svelte', 76 | }, 77 | 'sveltekit': { 78 | name: 'SvelteKit', 79 | match: (name) => name.startsWith('sveltekit'), 80 | } 81 | }; 82 | 83 | for (const [_, framework] of Object.entries(frameworkPatterns)) { 84 | if (framework.match(templateName)) { 85 | return { framework: framework.name, gen }; 86 | } 87 | } 88 | 89 | return null; 90 | }, 91 | 92 | getTemplateUrl(templateName) { 93 | return `${GITHUB_HTML_URL}/${ROOT_EXAMPLES_DIR}/${templateName}`; 94 | }, 95 | 96 | log: { 97 | info: (...args) => console.log('ℹ', ...args), 98 | success: (...args) => console.log(chalk.green('✔'), ...args), 99 | error: (...args) => console.log(chalk.red('✖'), ...args), 100 | warn: (...args) => console.log(chalk.yellow('⚠'), ...args), 101 | title: (...args) => console.log(chalk.bold('\n🔨', ...args)), 102 | } 103 | }; 104 | 105 | async function getExampleFolders() { 106 | try { 107 | const response = await axios.get(`${GITHUB_HTML_URL}/${ROOT_EXAMPLES_DIR}`, { 108 | headers: { 109 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' 110 | } 111 | }); 112 | 113 | const html = response.data; 114 | 115 | // Extract JSON data from the embedded script tag 116 | const jsonDataMatch = html.match(/