Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ describe('buildConfigFromArgs', () => {
expect(config.processes['format:check']).toEqual({ command: 'yarn format:check' })
})

test('npm run script:name derives process name from script', () => {
const config = buildConfigFromArgs(['npm run studio:dev'], [])
expect(config.processes['studio:dev']).toEqual({ command: 'npm run studio:dev' })
})

test('positional commands use first word for non-runner commands', () => {
const config = buildConfigFromArgs(['echo hello', '/usr/bin/node server.js'], [])
expect(config.processes.echo).toEqual({ command: 'echo hello' })
Expand Down
43 changes: 36 additions & 7 deletions src/config/expand-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,33 +366,62 @@ describe('expandScriptPatterns', () => {
})
const result = expandScriptPatterns({ processes: { 'lint:* --fix': {} } }, dir)
expect(Object.keys(result.processes).sort()).toEqual(['js', 'ts'])
expect(proc(result, 'js').command).toBe('npm run lint:js --fix')
expect(proc(result, 'ts').command).toBe('npm run lint:ts --fix')
expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix')
expect(proc(result, 'ts').command).toBe('npm run lint:ts -- --fix')
})

test('npm: prefix with extra args', () => {
const dir = setupDir('args-npm-prefix', {
'package.json': pkgJson({ 'lint:js': 'eslint', 'lint:ts': 'tsc' })
})
const result = expandScriptPatterns({ processes: { 'npm:lint:* --fix': {} } }, dir)
expect(proc(result, 'js').command).toBe('npm run lint:js --fix')
expect(proc(result, 'ts').command).toBe('npm run lint:ts --fix')
expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix')
expect(proc(result, 'ts').command).toBe('npm run lint:ts -- --fix')
})

test('multiple extra args forwarded', () => {
const dir = setupDir('args-multi', {
'package.json': pkgJson({ 'lint:js': 'eslint' })
})
const result = expandScriptPatterns({ processes: { 'lint:* --fix --quiet': {} } }, dir)
expect(proc(result, 'js').command).toBe('npm run lint:js --fix --quiet')
expect(proc(result, 'js').command).toBe('npm run lint:js -- --fix --quiet')
})

test('npm: exact script name with extra args', () => {
const dir = setupDir('args-exact', {
'package.json': pkgJson({ lint: 'eslint' })
})
const result = expandScriptPatterns({ processes: { 'npm:lint --fix': {} } }, dir)
expect(proc(result, 'lint').command).toBe('npm run lint --fix')
expect(proc(result, 'lint').command).toBe('npm run lint -- --fix')
})

test('npm:studio:dev shorthand expands to npm run studio:dev', () => {
const dir = setupDir('studio-dev', {
'package.json': pkgJson({ 'studio:dev': 'prisma studio' })
})
const result = expandScriptPatterns({ processes: { 'npm:studio:dev': { color: '#5A67D8' } } }, dir)
expect(proc(result, 'studio:dev').command).toBe('npm run studio:dev')
expect(proc(result, 'studio:dev').color).toBe('#5A67D8')
})

test('command value npm:script shorthand expands to pm run script', () => {
const dir = setupDir('cmd-shorthand', {
'package.json': pkgJson({ 'studio:dev': 'prisma studio' })
})
const result = expandScriptPatterns(
{ processes: { prisma: { command: 'npm:studio:dev', color: '#5A67D8' } } },
dir
)
expect(proc(result, 'prisma').command).toBe('npm run studio:dev')
expect(proc(result, 'prisma').color).toBe('#5A67D8')
})

test('command value npm:script with extra args', () => {
const dir = setupDir('cmd-shorthand-args', {
'package.json': pkgJson({ lint: 'eslint' })
})
const result = expandScriptPatterns({ processes: { lint: { command: 'npm:lint --fix' } } }, dir)
expect(proc(result, 'lint').command).toBe('npm run lint -- --fix')
})

test('prefix glob strips common prefix from process names', () => {
Expand Down Expand Up @@ -493,7 +522,7 @@ describe('expandScriptPatterns', () => {
'package.json': pkgJson({ 'lint:eslint': 'eslint .' })
})
const result = expandScriptPatterns({ processes: { 'lint:eslint --fix': {} } }, dir)
expect(proc(result, 'lint:eslint').command).toBe('npm run lint:eslint --fix')
expect(proc(result, 'lint:eslint').command).toBe('npm run lint:eslint -- --fix')
})

test('exact colon name inherits template properties', () => {
Expand Down
41 changes: 29 additions & 12 deletions src/config/expand-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,35 +71,52 @@ function splitPatternArgs(raw: string): { glob: string; extraArgs: string } {
return { glob: raw.slice(0, i), extraArgs: raw.slice(i) }
}

function expandScriptCommand(raw: string, pm: PackageManager): string {
const { glob: script, extraArgs } = splitPatternArgs(raw)
if (extraArgs) {
return `${pm} run ${script} --${extraArgs}`
}
return `${pm} run ${script}`
}

export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxConfig {
const entries = Object.entries(config.processes)
const cmd = (v: unknown) => (typeof v === 'string' ? v : (v as { command?: string })?.command)
const hasScriptRef = entries.some(([name, value]) => isScriptReference(name, value))
if (!hasScriptRef) return config
const hasNpmCommand = entries.some(([, v]) => {
const c = cmd(v)
return typeof c === 'string' && c.startsWith('npm:')
})
if (!(hasScriptRef || hasNpmCommand)) return config

const dir = config.cwd ?? cwd ?? process.cwd()
const pkgPath = resolve(dir, 'package.json')

if (!existsSync(pkgPath)) {
if (!existsSync(pkgPath) && hasScriptRef) {
throw new Error(`Wildcard patterns require a package.json (looked in ${dir})`)
}

const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>
const pkgJson = existsSync(pkgPath) ? (JSON.parse(readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>) : {}
const scripts = pkgJson.scripts as Record<string, string> | undefined
if (!scripts || typeof scripts !== 'object') {
throw new Error('package.json has no "scripts" field')
}

const scriptNames = Object.keys(scripts)
const scriptNames = scripts && typeof scripts === 'object' ? Object.keys(scripts) : []
const pm = detectPackageManager(pkgJson, dir)

const expanded: Record<string, NumuxProcessConfig | string> = {}

for (const [name, value] of entries) {
if (!isScriptReference(name, value)) {
expanded[name] = value as NumuxProcessConfig | string
let proc = value as NumuxProcessConfig | string
const c = cmd(proc)
if (typeof c === 'string' && c.startsWith('npm:')) {
const expandedCmd = expandScriptCommand(c.slice(4), pm)
proc = typeof proc === 'string' ? expandedCmd : { ...proc, command: expandedCmd }
}
expanded[name] = proc
continue
}

if (!scripts || typeof scripts !== 'object') {
throw new Error('package.json has no "scripts" field')
}

const rawPattern = name.startsWith('npm:') ? name.slice(4) : name
const { glob: globPattern, extraArgs } = splitPatternArgs(rawPattern)
const template = (value ?? {}) as Partial<NumuxProcessConfig>
Expand Down Expand Up @@ -143,7 +160,7 @@ export function expandScriptPatterns(config: NumuxConfig, cwd?: string): NumuxCo
const { color: _color, ...rest } = template
expanded[displayName] = {
...rest,
command: `${pm} run ${scriptName}${extraArgs}`,
command: expandScriptCommand(`${scriptName}${extraArgs}`, pm),
...(color ? { color } : {})
} as NumuxProcessConfig
}
Expand Down
4 changes: 3 additions & 1 deletion src/ui/prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ async function runPrefix(
extraArgs: string[] = [],
envOverrides: Record<string, string> = {}
): Promise<{ stdout: string; exitCode: number }> {
const env: Record<string, string | undefined> = { ...process.env, FORCE_COLOR: '0', ...envOverrides }
if (!('NO_COLOR' in envOverrides)) delete env.NO_COLOR
const proc = Bun.spawn(['bun', INDEX, '--prefix', ...extraArgs, '--config', configPath], {
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, FORCE_COLOR: '0', ...envOverrides }
env
})
const stdout = await new Response(proc.stdout).text()
const exitCode = await proc.exited
Expand Down
Loading