Created
July 9, 2025 06:36
-
-
Save ltomaszewski/8156251a3a19d5edbda5bb149d71b930 to your computer and use it in GitHub Desktop.
This script bootstraps a complete, production-ready monorepo for Node.js and TypeScript projects using NPM workspaces. It automates the entire setup process, creating a scalable and maintainable project structure with consistent tooling and configurations from the very start.
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 characters
| // 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(); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment