devserver-launch.ts
1 import fs from 'fs' 2 import path from 'path' 3 4 type FrameworkKind = 'next' | 'npm' | 'unknown' 5 6 interface PackageJsonLike { 7 scripts?: Record<string, unknown> 8 dependencies?: Record<string, unknown> 9 devDependencies?: Record<string, unknown> 10 } 11 12 export interface DevServerLaunchResolution { 13 inputDir: string 14 launchDir: string 15 packageRoot: string | null 16 framework: FrameworkKind 17 } 18 19 const NEXT_CONFIG_FILES = [ 20 'next.config.js', 21 'next.config.mjs', 22 'next.config', 23 ] 24 25 function readPackageJson(dir: string): PackageJsonLike | null { 26 const pkgPath = path.join(dir, 'package.json') 27 if (!fs.existsSync(pkgPath)) return null 28 try { 29 const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) 30 return parsed && typeof parsed === 'object' && !Array.isArray(parsed) 31 ? parsed as PackageJsonLike 32 : null 33 } catch { 34 return null 35 } 36 } 37 38 function hasNextDependency(pkg: PackageJsonLike): boolean { 39 const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } 40 return typeof deps.next === 'string' && deps.next.trim().length > 0 41 } 42 43 function hasNextScript(pkg: PackageJsonLike): boolean { 44 const scripts = Object.values(pkg.scripts || {}) 45 return scripts.some((value) => typeof value === 'string' && /\bnext\b/.test(value)) 46 } 47 48 function hasNextConfig(dir: string): boolean { 49 return NEXT_CONFIG_FILES.some((file) => fs.existsSync(path.join(dir, file))) 50 } 51 52 function classifyPackageRoot(dir: string, pkg: PackageJsonLike): FrameworkKind { 53 return hasNextDependency(pkg) || hasNextScript(pkg) || hasNextConfig(dir) 54 ? 'next' 55 : 'npm' 56 } 57 58 export function resolveDevServerLaunchDir(startDir: string): DevServerLaunchResolution { 59 const inputDir = path.resolve(startDir) 60 let current = inputDir 61 62 while (true) { 63 const pkg = readPackageJson(current) 64 if (pkg) { 65 const framework = classifyPackageRoot(current, pkg) 66 return { 67 inputDir, 68 launchDir: current, 69 packageRoot: current, 70 framework, 71 } 72 } 73 74 const parent = path.dirname(current) 75 if (parent === current) { 76 return { 77 inputDir, 78 launchDir: inputDir, 79 packageRoot: null, 80 framework: 'unknown', 81 } 82 } 83 current = parent 84 } 85 }