mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 14:33:11 +08:00
refactor: 重构脚本以统一 CLI 接口
This commit is contained in:
+3
-3
@@ -8,9 +8,9 @@
|
|||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx scripts/dev.ts",
|
"dev": "tsx scripts/cli.ts --mode=dev",
|
||||||
"build": "tsx scripts/build.ts",
|
"build": "tsx scripts/cli.ts --mode=build",
|
||||||
"serve": "tsx scripts/serve.ts",
|
"serve": "tsx scripts/cli.ts --mode=serve",
|
||||||
"lint": "run-s lint:tsc lint:eslint lint:stylelint",
|
"lint": "run-s lint:tsc lint:eslint lint:stylelint",
|
||||||
"lint:tsc": "pnpm --filter './apps/*' -r run lint",
|
"lint:tsc": "pnpm --filter './apps/*' -r run lint",
|
||||||
"lint:eslint": "eslint . --cache --fix",
|
"lint:eslint": "eslint . --cache --fix",
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawn } from 'node:child_process'
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import process from 'node:process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import * as p from '@clack/prompts'
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const rootDir = path.resolve(__dirname, '..')
|
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
|
|
||||||
let childProcess: ReturnType<typeof spawn> | null = null
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
if (childProcess) {
|
|
||||||
childProcess.kill('SIGINT')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interface AppInfo {
|
|
||||||
name: string
|
|
||||||
packageName: string
|
|
||||||
description: string
|
|
||||||
buildScripts: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getApps(): AppInfo[] {
|
|
||||||
const apps: AppInfo[] = []
|
|
||||||
|
|
||||||
// Scan apps directory (subdirectories)
|
|
||||||
const appsDir = path.resolve(rootDir, 'apps')
|
|
||||||
if (fs.existsSync(appsDir)) {
|
|
||||||
const entries = fs.readdirSync(appsDir, { withFileTypes: true })
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const packageJsonPath = path.join(appsDir, entry.name, 'package.json')
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
||||||
const buildScripts = Object.keys(pkg.scripts || {}).filter(s => s === 'build' || s.startsWith('build:'))
|
|
||||||
if (buildScripts.length > 0) {
|
|
||||||
apps.push({
|
|
||||||
name: entry.name,
|
|
||||||
packageName: pkg.name,
|
|
||||||
description: pkg.description || '',
|
|
||||||
buildScripts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runBuild(packageName: string, script: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
childProcess = spawn('pnpm', ['--filter', packageName, 'run', script], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: rootDir,
|
|
||||||
shell: isWindows,
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
childProcess = null
|
|
||||||
if (code === 0 || code === null) {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reject(new Error(`Process exited with code ${code}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('error', (err) => {
|
|
||||||
childProcess = null
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectBuildScript(app: AppInfo): Promise<string> {
|
|
||||||
if (app.buildScripts.length === 1) {
|
|
||||||
return app.buildScripts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = await p.select({
|
|
||||||
message: `Select build script for "${app.name}":`,
|
|
||||||
options: app.buildScripts.map(s => ({ value: s, label: s })),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p.isCancel(script)) {
|
|
||||||
p.cancel('Operation cancelled')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return script as string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
p.intro('Select apps to build')
|
|
||||||
|
|
||||||
const apps = getApps()
|
|
||||||
|
|
||||||
if (apps.length === 0) {
|
|
||||||
p.log.error('No apps found with build script')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apps.length === 1) {
|
|
||||||
p.log.info(`Only one app found: ${apps[0].name}`)
|
|
||||||
const script = await selectBuildScript(apps[0])
|
|
||||||
await runBuild(apps[0].packageName, script)
|
|
||||||
p.outro('Build complete!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const choices = apps.map(app => ({
|
|
||||||
value: app.packageName,
|
|
||||||
label: app.name,
|
|
||||||
hint: app.description,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const selected = await p.multiselect({
|
|
||||||
message: 'Which apps do you want to build?',
|
|
||||||
options: choices,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p.isCancel(selected)) {
|
|
||||||
p.cancel('Operation cancelled')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedPackageNames = selected as string[]
|
|
||||||
const selectedApps = selectedPackageNames.map(pkg => apps.find(a => a.packageName === pkg)!)
|
|
||||||
|
|
||||||
for (const app of selectedApps) {
|
|
||||||
const script = await selectBuildScript(app)
|
|
||||||
p.log.info(`Building "${app.name}" with script "${script}"...`)
|
|
||||||
await runBuild(app.packageName, script)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.outro('Build complete!')
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
p.log.error(err.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
+355
@@ -0,0 +1,355 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import * as p from '@clack/prompts'
|
||||||
|
|
||||||
|
interface PackageJson {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
scripts?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspaceEntry {
|
||||||
|
name: string
|
||||||
|
packageJsonPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppInfo {
|
||||||
|
name: string
|
||||||
|
packageName: string
|
||||||
|
description: string
|
||||||
|
scripts: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunMessageContext {
|
||||||
|
app: AppInfo
|
||||||
|
script: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModeConfig {
|
||||||
|
action: Mode
|
||||||
|
intro: string
|
||||||
|
noAppsMessage: string
|
||||||
|
appSelectMessage: string
|
||||||
|
scriptMatcher: (scriptName: string) => boolean
|
||||||
|
selectionMode: 'single' | 'multiple'
|
||||||
|
completionMessage?: string
|
||||||
|
getRunMessage?: (context: RunMessageContext) => string
|
||||||
|
showRunMessageWhenSingleApp?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode = 'build' | 'dev' | 'serve'
|
||||||
|
|
||||||
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const rootDir = path.resolve(currentDir, '..')
|
||||||
|
const isWindows = process.platform === 'win32'
|
||||||
|
const validModes: Mode[] = ['build', 'dev', 'serve']
|
||||||
|
const modeConfigs: Record<Mode, ModeConfig> = {
|
||||||
|
build: {
|
||||||
|
action: 'build',
|
||||||
|
intro: 'Select apps to build',
|
||||||
|
noAppsMessage: 'No apps found with build script',
|
||||||
|
appSelectMessage: 'Which apps do you want to build?',
|
||||||
|
scriptMatcher: isBuildScript,
|
||||||
|
selectionMode: 'multiple',
|
||||||
|
completionMessage: 'Build complete!',
|
||||||
|
getRunMessage: ({ app, script }) => `Building "${app.name}" with script "${script}"...`,
|
||||||
|
},
|
||||||
|
dev: {
|
||||||
|
action: 'dev',
|
||||||
|
intro: 'Select an app to run',
|
||||||
|
noAppsMessage: 'No apps found with dev script',
|
||||||
|
appSelectMessage: 'Which app do you want to run?',
|
||||||
|
scriptMatcher: isDevScript,
|
||||||
|
selectionMode: 'single',
|
||||||
|
getRunMessage: ({ app }) => `Starting ${app.packageName}...`,
|
||||||
|
},
|
||||||
|
serve: {
|
||||||
|
action: 'serve',
|
||||||
|
intro: 'Select an app to serve',
|
||||||
|
noAppsMessage: 'No apps found with serve script',
|
||||||
|
appSelectMessage: 'Which app do you want to serve?',
|
||||||
|
scriptMatcher: isServeScript,
|
||||||
|
selectionMode: 'single',
|
||||||
|
getRunMessage: ({ app, script }) => `Serving "${app.name}" with script "${script}"...`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let childProcess: ReturnType<typeof spawn> | null = null
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
childProcess?.kill('SIGINT')
|
||||||
|
})
|
||||||
|
|
||||||
|
function isBuildScript(scriptName: string): boolean {
|
||||||
|
return scriptName === 'build' || scriptName.startsWith('build:')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDevScript(scriptName: string): boolean {
|
||||||
|
return scriptName === 'dev'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isServeScript(scriptName: string): boolean {
|
||||||
|
return scriptName === 'serve' || scriptName.startsWith('serve:')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkspaceEntries(): WorkspaceEntry[] {
|
||||||
|
const entries: WorkspaceEntry[] = []
|
||||||
|
const appsDir = path.resolve(rootDir, 'apps')
|
||||||
|
|
||||||
|
if (fs.existsSync(appsDir)) {
|
||||||
|
const apps = fs.readdirSync(appsDir, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
if (!app.isDirectory()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(appsDir, app.name, 'package.json')
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
name: app.name,
|
||||||
|
packageJsonPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docsPackageJsonPath = path.resolve(rootDir, 'docs', 'package.json')
|
||||||
|
if (fs.existsSync(docsPackageJsonPath)) {
|
||||||
|
entries.push({
|
||||||
|
name: 'docs',
|
||||||
|
packageJsonPath: docsPackageJsonPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackageJson(packageJsonPath: string): PackageJson {
|
||||||
|
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApps(scriptMatcher: (scriptName: string) => boolean): AppInfo[] {
|
||||||
|
const apps: AppInfo[] = []
|
||||||
|
|
||||||
|
for (const entry of getWorkspaceEntries()) {
|
||||||
|
const packageJson = readPackageJson(entry.packageJsonPath)
|
||||||
|
const scripts = Object.keys(packageJson.scripts ?? {}).filter(scriptMatcher)
|
||||||
|
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
apps.push({
|
||||||
|
name: entry.name,
|
||||||
|
packageName: packageJson.name,
|
||||||
|
description: packageJson.description || '',
|
||||||
|
scripts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppChoices(apps: AppInfo[]): Array<{ value: string, label: string, hint: string }> {
|
||||||
|
return apps.map(app => ({
|
||||||
|
value: app.packageName,
|
||||||
|
label: app.name,
|
||||||
|
hint: app.description,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppByPackageName(apps: AppInfo[], packageName: string): AppInfo {
|
||||||
|
const app = apps.find(item => item.packageName === packageName)
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
throw new Error(`App not found: ${packageName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePromptValue<T>(value: T | symbol): T {
|
||||||
|
if (p.isCancel(value)) {
|
||||||
|
p.cancel('Operation cancelled')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectScript(app: AppInfo, action: Mode): Promise<string> {
|
||||||
|
if (app.scripts.length === 1) {
|
||||||
|
return app.scripts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = await p.select({
|
||||||
|
message: `Select ${action} script for "${app.name}":`,
|
||||||
|
options: app.scripts.map(name => ({
|
||||||
|
value: name,
|
||||||
|
label: name,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolvePromptValue(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPackageScript(packageName: string, script: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
childProcess = spawn('pnpm', ['--filter', packageName, 'run', script], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: rootDir,
|
||||||
|
shell: isWindows,
|
||||||
|
})
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
childProcess = null
|
||||||
|
|
||||||
|
if (code === 0 || code === null) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error(`Process exited with code ${code}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
childProcess.on('error', (error) => {
|
||||||
|
childProcess = null
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectApps(apps: AppInfo[], message: string, selectionMode: ModeConfig['selectionMode']): Promise<AppInfo[]> {
|
||||||
|
if (selectionMode === 'multiple') {
|
||||||
|
const selectedPackageNames = resolvePromptValue(await p.multiselect({
|
||||||
|
message,
|
||||||
|
options: getAppChoices(apps),
|
||||||
|
required: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return selectedPackageNames.map(packageName => getAppByPackageName(apps, packageName))
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPackageName = resolvePromptValue(await p.select({
|
||||||
|
message,
|
||||||
|
options: getAppChoices(apps),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [getAppByPackageName(apps, selectedPackageName)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSelectedApps(apps: AppInfo[], config: ModeConfig, showRunMessage: boolean): Promise<void> {
|
||||||
|
for (const app of apps) {
|
||||||
|
const script = await selectScript(app, config.action)
|
||||||
|
|
||||||
|
if (showRunMessage) {
|
||||||
|
const runMessage = config.getRunMessage?.({
|
||||||
|
app,
|
||||||
|
script,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (runMessage) {
|
||||||
|
p.log.info(runMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await runPackageScript(app.packageName, script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMode(config: ModeConfig): Promise<void> {
|
||||||
|
p.intro(config.intro)
|
||||||
|
|
||||||
|
const apps = getApps(config.scriptMatcher)
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
p.log.error(config.noAppsMessage)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apps.length === 1) {
|
||||||
|
p.log.info(`Only one app found: ${apps[0].name}`)
|
||||||
|
await runSelectedApps([apps[0]], config, config.showRunMessageWhenSingleApp ?? false)
|
||||||
|
|
||||||
|
if (config.completionMessage) {
|
||||||
|
p.outro(config.completionMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedApps = await selectApps(apps, config.appSelectMessage, config.selectionMode)
|
||||||
|
await runSelectedApps(selectedApps, config, true)
|
||||||
|
|
||||||
|
if (config.completionMessage) {
|
||||||
|
p.outro(config.completionMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseModeArg(argv: string[]): string | undefined {
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const arg = argv[index]
|
||||||
|
|
||||||
|
if (arg === '--mode' || arg === '-m') {
|
||||||
|
return argv[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('--mode=')) {
|
||||||
|
return arg.slice('--mode='.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith('-m=')) {
|
||||||
|
return arg.slice('-m='.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasHelpArg(argv: string[]): boolean {
|
||||||
|
return argv.includes('--help') || argv.includes('-h')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMode(value: string | undefined): value is Mode {
|
||||||
|
return value !== undefined && validModes.includes(value as Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp(): void {
|
||||||
|
console.log(`Usage: tsx scripts/cli.ts --mode=<${validModes.join('|')}>`)
|
||||||
|
console.log('Examples:')
|
||||||
|
console.log(' pnpm dev')
|
||||||
|
console.log(' pnpm build')
|
||||||
|
console.log(' pnpm serve')
|
||||||
|
console.log(' pnpm cli -- --mode=dev')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const argv = process.argv.slice(2)
|
||||||
|
const modeArg = parseModeArg(argv)
|
||||||
|
|
||||||
|
if (hasHelpArg(argv)) {
|
||||||
|
printHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMode(modeArg)) {
|
||||||
|
p.log.error(`Invalid or missing mode. Expected one of: ${validModes.join(', ')}`)
|
||||||
|
printHelp()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
await runMode(modeConfigs[modeArg])
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error: Error) => {
|
||||||
|
p.log.error(error.message)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawn } from 'node:child_process'
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import process from 'node:process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import * as p from '@clack/prompts'
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const rootDir = path.resolve(__dirname, '..')
|
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
|
|
||||||
let childProcess: ReturnType<typeof spawn> | null = null
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
if (childProcess) {
|
|
||||||
childProcess.kill('SIGINT')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interface AppInfo {
|
|
||||||
name: string
|
|
||||||
packageName: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function getApps(): AppInfo[] {
|
|
||||||
const apps: AppInfo[] = []
|
|
||||||
|
|
||||||
// Scan apps directory (subdirectories)
|
|
||||||
const appsDir = path.resolve(rootDir, 'apps')
|
|
||||||
if (fs.existsSync(appsDir)) {
|
|
||||||
const entries = fs.readdirSync(appsDir, { withFileTypes: true })
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const packageJsonPath = path.join(appsDir, entry.name, 'package.json')
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
||||||
if (pkg.scripts?.dev) {
|
|
||||||
apps.push({
|
|
||||||
name: entry.name,
|
|
||||||
packageName: pkg.name,
|
|
||||||
description: pkg.description || '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runDev(packageName: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
childProcess = spawn('pnpm', ['--filter', packageName, 'run', 'dev'], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: rootDir,
|
|
||||||
shell: isWindows,
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
childProcess = null
|
|
||||||
if (code === 0 || code === null) {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reject(new Error(`Process exited with code ${code}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('error', (err) => {
|
|
||||||
childProcess = null
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
p.intro('Select an app to run')
|
|
||||||
|
|
||||||
const apps = getApps()
|
|
||||||
|
|
||||||
if (apps.length === 0) {
|
|
||||||
p.log.error('No apps found with dev script')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apps.length === 1) {
|
|
||||||
p.log.info(`Only one app found: ${apps[0].name}`)
|
|
||||||
await runDev(apps[0].packageName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const choices = apps.map(app => ({
|
|
||||||
value: app.packageName,
|
|
||||||
label: app.name,
|
|
||||||
hint: app.description,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const selected = await p.select({
|
|
||||||
message: 'Which app do you want to run?',
|
|
||||||
options: choices,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p.isCancel(selected)) {
|
|
||||||
p.cancel('Operation cancelled')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.log.info(`Starting ${selected}...`)
|
|
||||||
await runDev(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
p.log.error(err.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { spawn } from 'node:child_process'
|
|
||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
import process from 'node:process'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import * as p from '@clack/prompts'
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
||||||
const rootDir = path.resolve(__dirname, '..')
|
|
||||||
const isWindows = process.platform === 'win32'
|
|
||||||
|
|
||||||
let childProcess: ReturnType<typeof spawn> | null = null
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
if (childProcess) {
|
|
||||||
childProcess.kill('SIGINT')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
interface AppInfo {
|
|
||||||
name: string
|
|
||||||
packageName: string
|
|
||||||
description: string
|
|
||||||
serveScripts: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getApps(): AppInfo[] {
|
|
||||||
const apps: AppInfo[] = []
|
|
||||||
|
|
||||||
// Scan apps directory (subdirectories)
|
|
||||||
const appsDir = path.resolve(rootDir, 'apps')
|
|
||||||
if (fs.existsSync(appsDir)) {
|
|
||||||
const entries = fs.readdirSync(appsDir, { withFileTypes: true })
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const packageJsonPath = path.join(appsDir, entry.name, 'package.json')
|
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
|
||||||
const serveScripts = Object.keys(pkg.scripts || {}).filter(s => s === 'serve' || s.startsWith('serve:'))
|
|
||||||
if (serveScripts.length > 0) {
|
|
||||||
apps.push({
|
|
||||||
name: entry.name,
|
|
||||||
packageName: pkg.name,
|
|
||||||
description: pkg.description || '',
|
|
||||||
serveScripts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apps
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runServe(packageName: string, script: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
childProcess = spawn('pnpm', ['--filter', packageName, 'run', script], {
|
|
||||||
stdio: 'inherit',
|
|
||||||
cwd: rootDir,
|
|
||||||
shell: isWindows,
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
childProcess = null
|
|
||||||
if (code === 0 || code === null) {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reject(new Error(`Process exited with code ${code}`))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
childProcess.on('error', (err) => {
|
|
||||||
childProcess = null
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectServeScript(app: AppInfo): Promise<string> {
|
|
||||||
if (app.serveScripts.length === 1) {
|
|
||||||
return app.serveScripts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const script = await p.select({
|
|
||||||
message: `Select serve script for "${app.name}":`,
|
|
||||||
options: app.serveScripts.map(s => ({ value: s, label: s })),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p.isCancel(script)) {
|
|
||||||
p.cancel('Operation cancelled')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return script as string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
p.intro('Select an app to serve')
|
|
||||||
|
|
||||||
const apps = getApps()
|
|
||||||
|
|
||||||
if (apps.length === 0) {
|
|
||||||
p.log.error('No apps found with serve script')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apps.length === 1) {
|
|
||||||
p.log.info(`Only one app found: ${apps[0].name}`)
|
|
||||||
const script = await selectServeScript(apps[0])
|
|
||||||
await runServe(apps[0].packageName, script)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const choices = apps.map(app => ({
|
|
||||||
value: app.packageName,
|
|
||||||
label: app.name,
|
|
||||||
hint: app.description,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const selected = await p.select({
|
|
||||||
message: 'Which app do you want to serve?',
|
|
||||||
options: choices,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (p.isCancel(selected)) {
|
|
||||||
p.cancel('Operation cancelled')
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedApp = apps.find(a => a.packageName === selected)!
|
|
||||||
const script = await selectServeScript(selectedApp)
|
|
||||||
p.log.info(`Serving "${selectedApp.name}" with script "${script}"...`)
|
|
||||||
await runServe(selectedApp.packageName, script)
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((err) => {
|
|
||||||
p.log.error(err.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user