Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e0c6546

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

2 files changed

+350-8Lines changed: 350 additions & 8 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/bunfig/src/config.ts‎

Copy file name to clipboardExpand all lines: packages/bunfig/src/config.ts
+29-3Lines 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}`)
Collapse file

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

Copy file name to clipboardExpand all lines: packages/bunfig/test/home-config-integration.test.ts
+321-5Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.