// 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] '); 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(); }); });