Skip to content

Instantly share code, notes, and snippets.

@ltomaszewski
Created July 9, 2025 06:36
Show Gist options
  • Save ltomaszewski/8156251a3a19d5edbda5bb149d71b930 to your computer and use it in GitHub Desktop.
Save ltomaszewski/8156251a3a19d5edbda5bb149d71b930 to your computer and use it in GitHub Desktop.

Revisions

  1. ltomaszewski created this gist Jul 9, 2025.
    338 changes: 338 additions & 0 deletions init-monorepo.js
    Original 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();
    });
    });