|
1 | 1 | #!/usr/bin/env node |
2 | | -// Simple multi-service dev runner (no turborepo / nx) |
3 | | -// Starts each workspace's dev script concurrently. |
4 | | -// Basic, no restart on file change; rely on each service's own dev behavior. |
5 | | -// CommonJS to ensure compatibility regardless of root type/module settings. |
| 2 | +// --------------------------------------------------------------------------- |
| 3 | +// Basic Multi-Service Dev Runner |
| 4 | +// --------------------------------------------------------------------------- |
| 5 | +// Purpose: |
| 6 | +// Lightweight script to start each service's "dev" script in parallel when |
| 7 | +// you are NOT using an advanced orchestrator (like Turborepo or Nx). |
| 8 | +// Intended for the scaffold's "basic" preset. |
| 9 | +// |
| 10 | +// High-Level Flow: |
| 11 | +// 1. Identify the root directory and the conventional services/ folder. |
| 12 | +// 2. Detect which JavaScript package manager to use (npm / pnpm / yarn / bun). |
| 13 | +// 3. Enumerate each subfolder in services/. |
| 14 | +// 4. For every folder containing a package.json with a dev script, spawn it. |
| 15 | +// 5. Stream each child process' output with a readable prefix. |
| 16 | +// |
| 17 | +// Design Constraints: |
| 18 | +// - Zero external dependencies (Node built‑ins only) to stay portable. |
| 19 | +// - Does NOT attempt file-change restarts (leave that to each service's own |
| 20 | +// tooling, e.g. nodemon, ts-node-dev, next dev, etc.). |
| 21 | +// - Ignores non-Node services (e.g., Go, Python, Java) since they don't have |
| 22 | +// a package.json dev script; those are expected to run via their own tools. |
| 23 | +// - CommonJS for maximum compatibility regardless of root "type: module". |
| 24 | +// |
| 25 | +// Extending Tips: |
| 26 | +// - To include non-Node services, add detection logic (e.g., main.go, pom.xml) |
| 27 | +// and spawn the appropriate commands. |
| 28 | +// - To implement auto-restart, consider integrating chokidar to watch files |
| 29 | +// and restart specific child processes. |
| 30 | +// - To add coloring per service, map service names to a color palette and |
| 31 | +// wrap the prefix() output (keep it optional for plain CI logs). |
| 32 | +// --------------------------------------------------------------------------- |
6 | 33 |
|
7 | 34 | const { spawn } = require('node:child_process'); |
8 | 35 | const fs = require('fs'); |
9 | 36 | const path = require('path'); |
10 | 37 |
|
| 38 | +// Root of the repo (assumes script is run from project root via npm script) |
11 | 39 | const root = process.cwd(); |
12 | | -// Updated to new structure: services directory replaces apps |
| 40 | + |
| 41 | +// Convention: all services generated by the scaffold live under services/ |
| 42 | +// (Older versions used apps/; that was replaced to avoid confusion with Next.js) |
13 | 43 | const servicesDir = path.join(root, 'services'); |
14 | 44 | if (!fs.existsSync(servicesDir)) { |
15 | 45 | console.error('No services directory found.'); |
16 | 46 | process.exit(1); |
17 | 47 | } |
18 | 48 |
|
| 49 | +// Snapshot the list of entries inside services/ (folders assumed to be services) |
19 | 50 | const services = fs.readdirSync(servicesDir); |
20 | 51 |
|
21 | 52 | // Determine the root package manager heuristically |
22 | 53 | function detectPM() { |
| 54 | + // Heuristic: look for lock files in order of preference. |
| 55 | + // If multiple exist (rare / misconfigured), first match wins. |
23 | 56 | if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'; |
24 | 57 | if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn'; |
25 | 58 | if (fs.existsSync(path.join(root, 'bun.lockb'))) return 'bun'; |
| 59 | + // Fallback to npm when no known lockfile is detected. |
26 | 60 | return 'npm'; |
27 | 61 | } |
28 | 62 | const pm = detectPM(); |
29 | 63 |
|
30 | 64 | if (services.length === 0) { |
| 65 | + // Nothing inside services/; we exit early (not an error condition). |
31 | 66 | console.log('No Node-based services with package.json to run.'); |
32 | 67 | process.exit(0); |
33 | 68 | } |
34 | 69 |
|
35 | 70 | console.log(`Using package manager: ${pm}`); |
36 | 71 | console.log(`Discovered services: ${services.join(', ')}`); |
37 | 72 |
|
| 73 | +// Keep references to spawned child processes so we can forward signals. |
38 | 74 | const procs = []; |
39 | 75 |
|
40 | 76 | function prefix(name, data) { |
| 77 | + // Prepend the service name to each line of output for readability. |
| 78 | + // NOTE: data may contain multiple lines; we leave as‑is to avoid splitting. |
41 | 79 | process.stdout.write(`[${name}] ${data}`); |
42 | 80 | } |
43 | 81 |
|
44 | 82 | services.forEach(svc => { |
45 | 83 | const svcPath = path.join(servicesDir, svc); |
46 | 84 | const pkgPath = path.join(svcPath, 'package.json'); |
47 | 85 | if (!fs.existsSync(pkgPath)) { |
| 86 | + // Non-Node service (e.g., python, go, java). Skip silently with context. |
48 | 87 | console.log(`Skipping ${svc} (no package.json; likely non-Node service)`); |
49 | 88 | return; |
50 | 89 | } |
51 | 90 | try { |
52 | 91 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); |
53 | 92 | if (!pkg.scripts || !pkg.scripts.dev) { |
| 93 | + // Node project present but without a dev script; user may add later. |
54 | 94 | console.log(`Skipping ${svc} (no dev script)`); |
55 | 95 | return; |
56 | 96 | } |
57 | | - const cmd = pm === 'bun' ? 'bun' : pm; |
| 97 | + // Determine spawn command + arguments. |
| 98 | + // For yarn classic, `yarn dev` would also work, but keeping uniform form. |
| 99 | + const cmd = pm === 'bun' ? 'bun' : pm; // direct binary (npm, pnpm, yarn, bun) |
58 | 100 | const args = pm === 'yarn' ? ['run', 'dev'] : pm === 'bun' ? ['run', 'dev'] : ['run', 'dev']; |
| 101 | + // Could special-case: if (pm==='yarn') args=['dev'] but current pattern is fine. |
59 | 102 | const child = spawn(cmd, args, { cwd: svcPath, shell: true, env: process.env }); |
60 | 103 | procs.push(child); |
61 | 104 | child.stdout.on('data', d => prefix(svc, d)); |
62 | 105 | child.stderr.on('data', d => prefix(svc, d)); |
63 | 106 | child.on('exit', code => { |
| 107 | + // Log exit so the developer sees early failures. |
64 | 108 | console.log(`[${svc}] exited with code ${code}`); |
65 | 109 | }); |
66 | 110 | } catch (e) { |
| 111 | + // JSON parse errors or spawn failures bubble here. |
67 | 112 | console.log(`Failed to start ${svc}:`, e.message); |
68 | 113 | } |
69 | 114 | }); |
70 | 115 |
|
71 | 116 | process.on('SIGINT', () => { |
| 117 | + // Graceful shutdown: forward Ctrl+C to each child, then exit. |
72 | 118 | procs.forEach(p => p.kill('SIGINT')); |
73 | 119 | process.exit(0); |
74 | 120 | }); |
0 commit comments