Created
          July 9, 2025 06:36 
        
      - 
      
 - 
        
Save ltomaszewski/8156251a3a19d5edbda5bb149d71b930 to your computer and use it in GitHub Desktop.  
Revisions
- 
        
ltomaszewski created this gist
Jul 9, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,338 @@ // Filename: init-monorepo.js // Description: A project-agnostic script to bootstrap a complete Node.js/TypeScript monorepo. // Usage: node init-monorepo.js const fs = require('fs'); const path =require('path'); const { execSync } = require('child_process'); const readline = require('readline'); // --- Helper Functions --- function createFile(filePath, content) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content.trim()); console.log(`✅ Created ${filePath}`); } function toPascalCase(str) { return str.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(''); } // --- File Content Templates --- const fileTemplates = { gitignore: ` # ## General Node.js ## # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* pnpm-debug.log* # Runtime data pids *.pid *.seed *.pid.lock # Dependency directories node_modules/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Output of 'npm pack' *.tgz # dotenv environment variables file .env .env.test .env.production .env.local .env.*.local # Mac files .DS_Store # ## TypeScript & Build Output ## /packages/*/dist /apps/*/dist *.tsbuildinfo # ## Project Specific ## .idea/ .vscode/ `, rootPackageJson: (projectName) => JSON.stringify({ name: projectName, private: true, version: "1.0.0", description: "A modular monorepo.", main: "index.js", scripts: { "create:package": `node scripts/create-package.js`, "create:app": `node scripts/create-package.js --app`, "build": "npx tsc --build", "test": "npm run test --workspaces", "clean": "npm run clean --workspaces" }, keywords: [], author: "", license: "ISC", workspaces: [ "packages/*", "apps/*" ] }, null, 2), jestConfig: ` /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { // Use ts-jest as a preset preset: 'ts-jest', // Set the test environment to Node.js testEnvironment: 'node', // Ignore the compiled 'dist' folder when running tests testPathIgnorePatterns: ['/node_modules/', '/dist/'], }; `, tsconfigBase: ` { "compilerOptions": { "target": "es2020", "module": "commonjs", "sourceMap": true, "strict": true, "moduleResolution": "node", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "noEmitOnError": true, "composite": true, "declaration": true, "declarationMap": true, "incremental": true } } `, rootTsconfig: ` { "files": [], "references": [] } `, createPackageScript: (scopeName) => ` const fs = require('fs'); const path = require('path'); const scope = '@${scopeName}'; const args = process.argv.slice(2); const isApp = args.includes('--app'); const packageName = args.find(arg => !arg.startsWith('--')); if (!packageName) { console.error('Error: Please provide a package name.'); console.log('Usage: node scripts/create-package.js [--app] <package-name>'); process.exit(1); } const baseDir = isApp ? 'apps' : 'packages'; const templatesDir = path.join(__dirname, 'templates'); function toPascalCase(str) { return str.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(''); } const className = toPascalCase(packageName); const fullPackageName = \`\${scope}/\${packageName}\`; const packagePath = path.join(baseDir, packageName); const srcPath = path.join(packagePath, 'src'); const testsPath = path.join(srcPath, '__tests__'); console.log(\`Creating \${isApp ? 'app' : 'package'}: \${fullPackageName} at \${packagePath}\`); if (fs.existsSync(packagePath)) { console.error(\`Error: Directory already exists at \${packagePath}\`); process.exit(1); } fs.mkdirSync(testsPath, { recursive: true }); function createFileFromTemplate(templateName, destinationPath, replacements = {}) { const templatePath = path.join(templatesDir, templateName); let content = fs.readFileSync(templatePath, 'utf8'); for (const key in replacements) { // Using string concatenation to build the RegExp to avoid nested template literal issues. content = content.replace(new RegExp('{{' + key + '}}', 'g'), replacements[key]); } fs.writeFileSync(destinationPath, content); } const packageJsonTemplate = JSON.parse(fs.readFileSync(path.join(templatesDir, 'package.json.template'), 'utf8')); const finalPackageJson = { name: fullPackageName, ...packageJsonTemplate }; fs.writeFileSync(path.join(packagePath, 'package.json'), JSON.stringify(finalPackageJson, null, 2)); createFileFromTemplate('tsconfig.json.template', path.join(packagePath, 'tsconfig.json')); createFileFromTemplate('jest.config.js.template', path.join(packagePath, 'jest.config.js')); createFileFromTemplate('index.ts.template', path.join(srcPath, 'index.ts'), { className }); createFileFromTemplate('index.test.ts.template', path.join(testsPath, 'index.test.ts'), { className }); const rootTsconfigPath = path.join(__dirname, '..', 'tsconfig.json'); try { const rootTsconfigContent = fs.readFileSync(rootTsconfigPath, 'utf8'); const rootTsconfigJson = JSON.parse(rootTsconfigContent); const newReference = { path: \`\${baseDir}/\${packageName}\` }; if (!rootTsconfigJson.references.some(ref => ref.path === newReference.path)) { rootTsconfigJson.references.push(newReference); rootTsconfigJson.references.sort((a, b) => a.path.localeCompare(b.path)); } fs.writeFileSync(rootTsconfigPath, JSON.stringify(rootTsconfigJson, null, 2) + '\\n'); console.log('✅ Automatically updated root tsconfig.json'); } catch (error) { console.error('⚠️ Could not update root tsconfig.json. Please add the reference manually.', error); } console.log(\`✅ Successfully created \${fullPackageName}.\`); console.log('Next steps:'); console.log('Run \`npm install\` to link the new package.'); `, scaffold_packageJson: ` { "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc --build", "test": "tsc --build && jest", "clean": "rm -rf dist tsconfig.tsbuildinfo" }, "dependencies": {}, "description": "", "keywords": [], "author": "", "license": "ISC", "type": "commonjs" } `, scaffold_tsconfig: ` { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"], "references": [] } `, scaffold_jestConfig: ` // Import the root configuration const rootConfig = require("../../jest.config.js"); module.exports = { // Extend the root configuration ...rootConfig, }; `, scaffold_indexTs: ` /** * A utility class for {{className}}. */ export class {{className}} { /** * Returns a greeting string. * @returns A string that says "Hello from {{className}}!". */ public static getGreeting(): string { return "Hello from {{className}}!"; } } `, scaffold_indexTestTs: ` import { {{className}} } from "../index"; describe("{{className}}", () => { it("should return a greeting", () => { expect({{className}}.getGreeting()).toBe("Hello from {{className}}!"); }); }); ` }; // --- Main Execution --- const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('Enter the project name (e.g., my-new-project): ', (projectName) => { rl.question('Enter the NPM scope name (e.g., my-org): ', (scopeName) => { console.log(`\n🚀 Initializing monorepo "${projectName}" with scope "@${scopeName}"...`); // Create root project directory if (fs.existsSync(projectName)) { console.error(`Error: Directory "${projectName}" already exists.`); rl.close(); return; } fs.mkdirSync(projectName); process.chdir(projectName); // Create core config files from templates createFile('.gitignore', fileTemplates.gitignore); createFile('package.json', fileTemplates.rootPackageJson(projectName)); createFile('jest.config.js', fileTemplates.jestConfig); createFile('tsconfig.base.json', fileTemplates.tsconfigBase); createFile('tsconfig.json', fileTemplates.rootTsconfig); // Create scaffolding script and its templates const scriptsDir = 'scripts'; const templatesDir = path.join(scriptsDir, 'templates'); fs.mkdirSync(templatesDir, { recursive: true }); createFile(path.join(scriptsDir, 'create-package.js'), fileTemplates.createPackageScript(scopeName)); createFile(path.join(templatesDir, 'package.json.template'), fileTemplates.scaffold_packageJson); createFile(path.join(templatesDir, 'tsconfig.json.template'), fileTemplates.scaffold_tsconfig); createFile(path.join(templatesDir, 'jest.config.js.template'), fileTemplates.scaffold_jestConfig); createFile(path.join(templatesDir, 'index.ts.template'), fileTemplates.scaffold_indexTs); createFile(path.join(templatesDir, 'index.test.ts.template'), fileTemplates.scaffold_indexTestTs); // Install root dependencies console.log('\n📦 Installing root dependencies...'); try { execSync('npm install typescript ts-node jest ts-jest @types/node @types/jest --save-dev', { stdio: 'inherit' }); } catch (error) { console.error('\n❌ Failed to install root dependencies.'); process.exit(1); } console.log('\n🎉 Monorepo setup complete!'); console.log(`\nNavigate to your new project: \`cd ${projectName}\``); console.log('To create your first package, run: `npm run create:package -- my-first-package`'); rl.close(); }); });