Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e0c6546

Browse files
committed
feat: allow for ~/.<name>.* configs
chore: wip
1 parent cd8513b commit e0c6546

File tree

2 files changed

+350
-8
lines changed

2 files changed

+350
-8
lines changed

packages/bunfig/src/config.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,39 @@ export async function loadConfig<T>({
266266
}
267267
}
268268

269-
// Also try dotfile configs in user's home directory root (e.g., ~/.<name>.config.*)
269+
// Try loading dotfile configs from user's home config directory (~/.config/.<name>.config.*)
270270
if (name) {
271-
const homeDir = homedir()
272-
const homeRootPatterns = [`.${name}.config`]
271+
const homeConfigDir = resolve(homedir(), '.config')
272+
const homeConfigDotfilePatterns = [`.${name}.config`]
273273

274274
if (alias)
275+
homeConfigDotfilePatterns.push(`.${alias}.config`)
276+
277+
if (verbose)
278+
log.info(`Checking user config directory for dotfile configs: ${homeConfigDir}`)
279+
280+
for (const configPath of homeConfigDotfilePatterns) {
281+
for (const ext of extensions) {
282+
const fullPath = resolve(homeConfigDir, `${configPath}${ext}`)
283+
const config = await tryLoadConfig(fullPath, configWithEnvVars, arrayStrategy)
284+
if (config !== null) {
285+
if (verbose)
286+
log.success(`Configuration loaded from user config directory dotfile: ${fullPath}`)
287+
return config
288+
}
289+
}
290+
}
291+
}
292+
293+
// Also try dotfile configs in user's home directory root (e.g., ~/.<name>.config.* and ~/.<name>.*)
294+
if (name) {
295+
const homeDir = homedir()
296+
const homeRootPatterns = [`.${name}.config`, `.${name}`]
297+
298+
if (alias) {
275299
homeRootPatterns.push(`.${alias}.config`)
300+
homeRootPatterns.push(`.${alias}`)
301+
}
276302

277303
if (verbose)
278304
log.info(`Checking user home directory for dotfile configs: ${homeDir}`)

packages/bunfig/test/home-config-integration.test.ts

Lines changed: 321 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,53 @@ describe('Home Config Directory Integration Tests', () => {
3434
}
3535

3636
// Helper to clean up root-level home dotfiles for a specific test name/alias
37-
const _cleanupHomeDotfiles = (testName: string, alias?: string) => {
37+
const cleanupHomeDotfiles = (testName: string, alias?: string) => {
3838
const home = homedir()
39-
const files = [`.${testName}.config.ts`, `.${testName}.config.js`, `.${testName}.config.mjs`, `.${testName}.config.cjs`, `.${testName}.config.json`]
39+
const homeConfigDir = resolve(home, '.config')
40+
41+
// Clean up root-level dotfiles (~/.<name>.config.* and ~/.<name>.*)
42+
const rootFiles = [
43+
`.${testName}.config.ts`,
44+
`.${testName}.config.js`,
45+
`.${testName}.config.mjs`,
46+
`.${testName}.config.cjs`,
47+
`.${testName}.config.json`,
48+
`.${testName}.ts`,
49+
`.${testName}.js`,
50+
`.${testName}.mjs`,
51+
`.${testName}.cjs`,
52+
`.${testName}.json`,
53+
]
4054
if (alias) {
41-
files.push(`.${alias}.config.ts`, `.${alias}.config.js`, `.${alias}.config.mjs`, `.${alias}.config.cjs`, `.${alias}.config.json`)
55+
rootFiles.push(
56+
`.${alias}.config.ts`,
57+
`.${alias}.config.js`,
58+
`.${alias}.config.mjs`,
59+
`.${alias}.config.cjs`,
60+
`.${alias}.config.json`,
61+
`.${alias}.ts`,
62+
`.${alias}.js`,
63+
`.${alias}.mjs`,
64+
`.${alias}.cjs`,
65+
`.${alias}.json`,
66+
)
4267
}
43-
for (const f of files) {
68+
for (const f of rootFiles) {
4469
const p = resolve(home, f)
4570
if (existsSync(p))
4671
rmSync(p)
4772
}
73+
74+
// Clean up ~/.config dotfiles (~/.config/.<name>.config.*)
75+
const configDirFiles = [`.${testName}.config.ts`, `.${testName}.config.js`, `.${testName}.config.mjs`, `.${testName}.config.cjs`, `.${testName}.config.json`]
76+
if (alias) {
77+
configDirFiles.push(`.${alias}.config.ts`, `.${alias}.config.js`, `.${alias}.config.mjs`, `.${alias}.config.cjs`, `.${alias}.config.json`)
78+
}
79+
for (const f of configDirFiles) {
80+
const p = resolve(homeConfigDir, f)
81+
if (existsSync(p))
82+
rmSync(p)
83+
}
4884
}
4985

5086
describe('Real home config loading', () => {
@@ -76,7 +112,7 @@ describe('Home Config Directory Integration Tests', () => {
76112
})
77113
}
78114
finally {
79-
_cleanupHomeDotfiles(testName)
115+
cleanupHomeDotfiles(testName)
80116
}
81117
})
82118

@@ -393,4 +429,284 @@ describe('Home Config Directory Integration Tests', () => {
393429
}
394430
})
395431
})
432+
433+
describe('Dotfile config patterns', () => {
434+
describe('~/.config/.<name>.config.* patterns', () => {
435+
it('should load config from ~/.config/.<name>.config.ts', async () => {
436+
const testName = generateTestName()
437+
const homeConfigDir = resolve(homedir(), '.config')
438+
439+
try {
440+
mkdirSync(homeConfigDir, { recursive: true })
441+
442+
const configPath = resolve(homeConfigDir, `.${testName}.config.ts`)
443+
writeFileSync(configPath, `export default { source: 'config-dir-dotfile', value: 42 }`)
444+
445+
const result = await loadConfig({
446+
name: testName,
447+
cwd: testCwd,
448+
defaultConfig: { source: 'default', value: 0 },
449+
})
450+
451+
expect(result).toEqual({ source: 'config-dir-dotfile', value: 42 })
452+
}
453+
finally {
454+
cleanupHomeDotfiles(testName)
455+
}
456+
})
457+
458+
it('should load config from ~/.config/.<alias>.config.ts when using alias', async () => {
459+
const testName = generateTestName()
460+
const testAlias = `${testName}-alias`
461+
const homeConfigDir = resolve(homedir(), '.config')
462+
463+
try {
464+
mkdirSync(homeConfigDir, { recursive: true })
465+
466+
const configPath = resolve(homeConfigDir, `.${testAlias}.config.ts`)
467+
writeFileSync(configPath, `export default { source: 'config-dir-alias-dotfile', alias: true }`)
468+
469+
const result = await loadConfig({
470+
name: testName,
471+
alias: testAlias,
472+
cwd: testCwd,
473+
defaultConfig: { source: 'default', alias: false },
474+
})
475+
476+
expect(result).toEqual({ source: 'config-dir-alias-dotfile', alias: true })
477+
}
478+
finally {
479+
cleanupHomeDotfiles(testName, testAlias)
480+
}
481+
})
482+
483+
it('should handle different extensions for ~/.config/.<name>.config.*', async () => {
484+
const testName = generateTestName()
485+
const homeConfigDir = resolve(homedir(), '.config')
486+
487+
try {
488+
mkdirSync(homeConfigDir, { recursive: true })
489+
490+
const configPath = resolve(homeConfigDir, `.${testName}.config.json`)
491+
writeFileSync(configPath, JSON.stringify({ source: 'config-dir-dotfile-json', format: 'json' }))
492+
493+
const result = await loadConfig({
494+
name: testName,
495+
cwd: testCwd,
496+
defaultConfig: { source: 'default', format: 'unknown' },
497+
})
498+
499+
expect(result).toEqual({ source: 'config-dir-dotfile-json', format: 'json' })
500+
}
501+
finally {
502+
cleanupHomeDotfiles(testName)
503+
}
504+
})
505+
})
506+
507+
describe('~/.<name>.* patterns (without .config suffix)', () => {
508+
it('should load config from ~/.<name>.ts', async () => {
509+
const testName = generateTestName()
510+
const homeDir = homedir()
511+
512+
try {
513+
const configPath = resolve(homeDir, `.${testName}.ts`)
514+
writeFileSync(configPath, `export default { source: 'home-root-dotfile', simple: true }`)
515+
516+
const result = await loadConfig({
517+
name: testName,
518+
cwd: testCwd,
519+
defaultConfig: { source: 'default', simple: false },
520+
})
521+
522+
expect(result).toEqual({ source: 'home-root-dotfile', simple: true })
523+
}
524+
finally {
525+
cleanupHomeDotfiles(testName)
526+
}
527+
})
528+
529+
it('should load config from ~/.<alias>.ts when using alias', async () => {
530+
const testName = generateTestName()
531+
const testAlias = `${testName}-alias`
532+
const homeDir = homedir()
533+
534+
try {
535+
const configPath = resolve(homeDir, `.${testAlias}.ts`)
536+
writeFileSync(configPath, `export default { source: 'home-root-alias-dotfile', aliased: true }`)
537+
538+
const result = await loadConfig({
539+
name: testName,
540+
alias: testAlias,
541+
cwd: testCwd,
542+
defaultConfig: { source: 'default', aliased: false },
543+
})
544+
545+
expect(result).toEqual({ source: 'home-root-alias-dotfile', aliased: true })
546+
}
547+
finally {
548+
cleanupHomeDotfiles(testName, testAlias)
549+
}
550+
})
551+
552+
it('should handle different extensions for ~/.<name>.*', async () => {
553+
const testName = generateTestName()
554+
const homeDir = homedir()
555+
556+
try {
557+
const configPath = resolve(homeDir, `.${testName}.json`)
558+
writeFileSync(configPath, JSON.stringify({ source: 'home-root-dotfile-json', extension: 'json' }))
559+
560+
const result = await loadConfig({
561+
name: testName,
562+
cwd: testCwd,
563+
defaultConfig: { source: 'default', extension: 'unknown' },
564+
})
565+
566+
expect(result).toEqual({ source: 'home-root-dotfile-json', extension: 'json' })
567+
}
568+
finally {
569+
cleanupHomeDotfiles(testName)
570+
}
571+
})
572+
})
573+
574+
describe('Config loading priority', () => {
575+
it('should prefer ~/.config/.<name>.config.* over ~/.<name>.*', async () => {
576+
const testName = generateTestName()
577+
const homeDir = homedir()
578+
const homeConfigDir = resolve(homeDir, '.config')
579+
580+
try {
581+
mkdirSync(homeConfigDir, { recursive: true })
582+
583+
// Create both patterns
584+
const configDirPath = resolve(homeConfigDir, `.${testName}.config.ts`)
585+
const rootPath = resolve(homeDir, `.${testName}.ts`)
586+
587+
writeFileSync(configDirPath, `export default { source: 'config-dir-dotfile', priority: 'high' }`)
588+
writeFileSync(rootPath, `export default { source: 'home-root-dotfile', priority: 'low' }`)
589+
590+
const result = await loadConfig({
591+
name: testName,
592+
cwd: testCwd,
593+
defaultConfig: { source: 'default', priority: 'none' },
594+
})
595+
596+
// Should prefer ~/.config/.<name>.config.* over ~/.<name>.*
597+
expect(result).toEqual({ source: 'config-dir-dotfile', priority: 'high' })
598+
}
599+
finally {
600+
cleanupHomeDotfiles(testName)
601+
}
602+
})
603+
604+
it('should prefer ~/.<name>.config.* over ~/.<name>.*', async () => {
605+
const testName = generateTestName()
606+
const homeDir = homedir()
607+
608+
try {
609+
// Create both patterns in home root
610+
const configPath = resolve(homeDir, `.${testName}.config.ts`)
611+
const simplePath = resolve(homeDir, `.${testName}.ts`)
612+
613+
writeFileSync(configPath, `export default { source: 'home-config-dotfile', priority: 'high' }`)
614+
writeFileSync(simplePath, `export default { source: 'home-simple-dotfile', priority: 'low' }`)
615+
616+
const result = await loadConfig({
617+
name: testName,
618+
cwd: testCwd,
619+
defaultConfig: { source: 'default', priority: 'none' },
620+
})
621+
622+
// Should prefer ~/.<name>.config.* over ~/.<name>.*
623+
expect(result).toEqual({ source: 'home-config-dotfile', priority: 'high' })
624+
}
625+
finally {
626+
cleanupHomeDotfiles(testName)
627+
}
628+
})
629+
630+
it('should prefer local config over all dotfile patterns', async () => {
631+
const testName = generateTestName()
632+
const homeDir = homedir()
633+
const homeConfigDir = resolve(homeDir, '.config')
634+
635+
try {
636+
mkdirSync(homeConfigDir, { recursive: true })
637+
638+
// Create local config
639+
const localConfigPath = resolve(testCwd, `${testName}.config.ts`)
640+
writeFileSync(localConfigPath, `export default { source: 'local', priority: 'highest' }`)
641+
642+
// Create all dotfile patterns
643+
const configDirPath = resolve(homeConfigDir, `.${testName}.config.ts`)
644+
const homeConfigPath = resolve(homeDir, `.${testName}.config.ts`)
645+
const homeSimplePath = resolve(homeDir, `.${testName}.ts`)
646+
647+
writeFileSync(configDirPath, `export default { source: 'config-dir-dotfile', priority: 'high' }`)
648+
writeFileSync(homeConfigPath, `export default { source: 'home-config-dotfile', priority: 'medium' }`)
649+
writeFileSync(homeSimplePath, `export default { source: 'home-simple-dotfile', priority: 'low' }`)
650+
651+
const result = await loadConfig({
652+
name: testName,
653+
cwd: testCwd,
654+
defaultConfig: { source: 'default', priority: 'none' },
655+
})
656+
657+
// Should prefer local config over all dotfile patterns
658+
expect(result).toEqual({ source: 'local', priority: 'highest' })
659+
}
660+
finally {
661+
cleanupHomeDotfiles(testName)
662+
}
663+
})
664+
})
665+
666+
describe('Error handling for dotfile patterns', () => {
667+
it('should handle invalid ~/.config/.<name>.config.* gracefully', async () => {
668+
const testName = generateTestName()
669+
const homeConfigDir = resolve(homedir(), '.config')
670+
671+
try {
672+
mkdirSync(homeConfigDir, { recursive: true })
673+
674+
const configPath = resolve(homeConfigDir, `.${testName}.config.ts`)
675+
writeFileSync(configPath, `export default "invalid config"`)
676+
677+
const result = await loadConfig({
678+
name: testName,
679+
cwd: testCwd,
680+
defaultConfig: { source: 'default', valid: true },
681+
})
682+
683+
expect(result).toEqual({ source: 'default', valid: true })
684+
}
685+
finally {
686+
cleanupHomeDotfiles(testName)
687+
}
688+
})
689+
690+
it('should handle invalid ~/.<name>.* gracefully', async () => {
691+
const testName = generateTestName()
692+
const homeDir = homedir()
693+
694+
try {
695+
const configPath = resolve(homeDir, `.${testName}.ts`)
696+
writeFileSync(configPath, `export default null`)
697+
698+
const result = await loadConfig({
699+
name: testName,
700+
cwd: testCwd,
701+
defaultConfig: { source: 'default', valid: true },
702+
})
703+
704+
expect(result).toEqual({ source: 'default', valid: true })
705+
}
706+
finally {
707+
cleanupHomeDotfiles(testName)
708+
}
709+
})
710+
})
711+
})
396712
})

0 commit comments

Comments
 (0)