Skip to content

Commit 32e7f8e

Browse files
committedFeb 25, 2025
chore: several minor updates
1 parent d407340 commit 32e7f8e

File tree

10 files changed

+282
-79
lines changed

10 files changed

+282
-79
lines changed
 

Diff for: ‎.vscode/dictionary.txt

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ destructurable
1818
dnsx
1919
dtsx
2020
entrypoints
21+
happydom
2122
heroicons
2223
httx
2324
iconify
@@ -33,6 +34,7 @@ Postcardware
3334
prefetch
3435
preinstall
3536
quickfix
37+
Registrator
3638
shikijs
3739
socio
3840
softprops
@@ -51,5 +53,6 @@ vidx
5153
vite
5254
vitebook
5355
vitejs
56+
vitepress
5457
vue-demi
5558
vueus

Diff for: ‎README.md

+46-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ bun install -d bunfig
2727

2828
## Get Started
2929

30-
If you are building any sort of Bun project, you can use the `loadConfig` function to load your configuration.
30+
### Server Environment
31+
32+
If you are building any sort of Bun project, you can use the `loadConfig` function to load your configuration from files:
3133

3234
```ts
3335
import type { Config } from 'bunfig'
@@ -41,7 +43,7 @@ interface MyLibraryConfig {
4143
const options: Config<MyLibraryConfig> = {
4244
name: 'my-app', // required
4345
cwd: './', // default: process.cwd()
44-
defaults: { // default: {}
46+
defaultConfig: { // default: {}
4547
port: 3000,
4648
host: 'localhost',
4749
},
@@ -55,7 +57,45 @@ console.log(resolvedConfig) // { port: 3000, host: 'localhost' }, unless a confi
5557
> [!TIP]
5658
> If your `process.cwd()` includes a `$name.config.{ts,js,mjs,cjs,json}` _(or `.$name.config.{ts,js,mjs,cjs,json}`)_ file, it will be loaded and merged with defaults, where file config file values take precedence. For minimalists, it also loads a `.$name.{ts,js,mjs,cjs,json}` and `$name.{ts,js,mjs,cjs,json}` file if present.
5759
58-
Alternatively, you can use the `config` function to load your configuration.
60+
### Browser Environment
61+
62+
For browser environments, use the `loadConfig` function from the browser-specific entry point to load your configuration from an API endpoint:
63+
64+
```ts
65+
import type { Config } from 'bunfig'
66+
import { loadConfig } from 'bunfig/browser'
67+
68+
interface MyLibraryConfig {
69+
port: number
70+
host: string
71+
}
72+
73+
const options: Config<MyLibraryConfig> = {
74+
name: 'my-app',
75+
endpoint: '/api/config', // required for browser environment
76+
defaultConfig: {
77+
port: 3000,
78+
host: 'localhost',
79+
},
80+
headers: { // optional custom headers
81+
'Authorization': 'Bearer token',
82+
'X-Custom-Header': 'value',
83+
},
84+
}
85+
86+
const resolvedConfig = await loadConfig(options)
87+
```
88+
89+
In the browser:
90+
91+
- The `endpoint` parameter is required to specify where to fetch the configuration
92+
- Custom headers can be provided to authenticate or customize the request
93+
- Default headers (`Accept` and `Content-Type`) are automatically included
94+
- If the fetch fails, the default configuration is used as a fallback
95+
96+
### Alternative Usage
97+
98+
Alternatively, you can use the `config` function to load your configuration in server environments:
5999

60100
```ts
61101
import type { Config } from 'bunfig'
@@ -69,7 +109,7 @@ interface MyAppOrLibraryConfig {
69109
const options: Config<MyAppOrLibraryConfig> = {
70110
name: 'my-app', // required to know which config file to load
71111
cwd: './', // default: process.cwd()
72-
defaults: { // default: {}
112+
defaultConfig: { // default: {}
73113
port: 3000,
74114
host: 'localhost',
75115
},
@@ -83,8 +123,8 @@ The config function is a wrapper around the `loadConfig` function and is useful
83123
- `name`: The name of the config file to load.
84124
- `cwd`: The current working directory to load the config file from.
85125
- `defaultConfig`: The default config to use if no config file is found.
86-
- `endpoint`: The endpoint to fetch the config from.
87-
- `headers`: The headers to send to the endpoint.
126+
127+
For browser usage, see the [Browser Environment](#browser-environment) section above.
88128

89129
## Testing
90130

Diff for: ‎build.ts

+11
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import { dts } from 'bun-plugin-dtsx'
33

44
console.log('Building...')
55

6+
// Build the main package
67
await Bun.build({
78
entrypoints: ['src/index.ts'],
89
outdir: './dist',
10+
target: 'bun',
11+
plugins: [dts()],
12+
})
13+
14+
// Build the browser version
15+
await Bun.build({
16+
entrypoints: ['src/browser.ts'],
17+
outdir: './dist/browser',
18+
target: 'browser',
919
plugins: [dts()],
1020
})
1121

22+
// Build the CLI
1223
await Bun.build({
1324
entrypoints: ['bin/cli.ts'],
1425
outdir: './dist',

Diff for: ‎bun.lock

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"": {
55
"name": "bunfig",
66
"devDependencies": {
7+
"@happy-dom/global-registrator": "^17.1.8",
78
"@iconify-json/carbon": "^1.2.7",
89
"@shikijs/vitepress-twoslash": "^3.0.0",
910
"@stacksjs/eslint-config": "^4.2.1-beta.1",
@@ -331,6 +332,8 @@
331332

332333
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
333334

335+
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@17.1.8", "", { "dependencies": { "happy-dom": "^17.1.8" } }, "sha512-8/INgMD5gqzhaGnRbcHvQ3cYa70ZbdUTMiCQg+4Pz22vogILU2Q1spnneunMVjAtx6DBRMO8rBnDeMREVVyADQ=="],
336+
334337
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
335338

336339
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@@ -967,6 +970,8 @@
967970

968971
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
969972

973+
"happy-dom": ["happy-dom@17.1.8", "", { "dependencies": { "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" } }, "sha512-Yxbq/FG79z1rhAf/iB6YM8wO2JB/JDQBy99RiLSs+2siEAi5J05x9eW1nnASHZJbpldjJE2KuFLsLZ+AzX/IxA=="],
974+
970975
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
971976

972977
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@@ -1669,10 +1674,12 @@
16691674

16701675
"vue-resize": ["vue-resize@2.0.0-alpha.1", "", { "peerDependencies": { "vue": "^3.0.0" } }, "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg=="],
16711676

1672-
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
1677+
"webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
16731678

16741679
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
16751680

1681+
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
1682+
16761683
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
16771684

16781685
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1955,6 +1962,8 @@
19551962

19561963
"vue-eslint-parser/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
19571964

1965+
"whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
1966+
19581967
"workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
19591968

19601969
"workbox-build/rollup": ["rollup@2.79.2", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ=="],

Diff for: ‎bunfig.toml

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[install]
22
registry = { url = "https://registry.npmjs.org/", token = "$BUN_AUTH_TOKEN" }
3+
4+
[test]
5+
preload = "./happydom.ts"

Diff for: ‎happydom.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { GlobalRegistrator } from '@happy-dom/global-registrator'
2+
3+
GlobalRegistrator.register()

Diff for: ‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"typecheck": "bun --bun tsc --noEmit"
5252
},
5353
"devDependencies": {
54+
"@happy-dom/global-registrator": "^17.1.8",
5455
"@iconify-json/carbon": "^1.2.7",
5556
"@shikijs/vitepress-twoslash": "^3.0.0",
5657
"@stacksjs/eslint-config": "^4.2.1-beta.1",

Diff for: ‎src/browser.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { Config } from './types'
2+
import { deepMerge } from './utils'
3+
4+
/**
5+
* Load config in browser environment
6+
*
7+
* @param name - The name of the configuration
8+
* @param endpoint - The API endpoint to fetch config from
9+
* @param defaultConfig - The default configuration
10+
* @param headers - Optional headers to include in the request
11+
* @returns The merged configuration
12+
*/
13+
export async function loadConfig<T>({
14+
name: _name,
15+
endpoint,
16+
defaultConfig,
17+
headers = {
18+
'Accept': 'application/json',
19+
'Content-Type': 'application/json',
20+
},
21+
}: Pick<Config<T>, 'name' | 'endpoint' | 'defaultConfig' | 'headers'>): Promise<T> {
22+
if (!endpoint) {
23+
console.warn('An API endpoint is required to load the client config.')
24+
return defaultConfig
25+
}
26+
27+
try {
28+
const response = await fetch(endpoint, {
29+
method: 'GET',
30+
headers: {
31+
'Accept': 'application/json',
32+
'Content-Type': 'application/json',
33+
...headers,
34+
},
35+
})
36+
37+
if (!response.ok)
38+
throw new Error(`HTTP error! status: ${response.status}`)
39+
40+
const loadedConfig = await response.json() as T
41+
42+
// Validate that the loaded config can be merged with the default config
43+
try {
44+
return deepMerge(defaultConfig, loadedConfig) as T
45+
}
46+
catch {
47+
return defaultConfig
48+
}
49+
}
50+
catch (error) {
51+
console.error('Failed to load client config:', error)
52+
return defaultConfig
53+
}
54+
}
55+
56+
/**
57+
* Check if code is running in a browser environment
58+
*/
59+
export function isBrowser(): boolean {
60+
return typeof window !== 'undefined' && typeof fetch === 'function'
61+
}

Diff for: ‎src/config.ts

+21-72
Original file line numberDiff line numberDiff line change
@@ -58,94 +58,43 @@ export async function tryLoadConfig<T>(configPath: string, defaultConfig: T): Pr
5858
* @param {object} options - The configuration options.
5959
* @param {string} options.name - The name of the configuration file.
6060
* @param {string} [options.cwd] - The current working directory.
61-
* @param {string} [options.endpoint] - The API endpoint to fetch config from in browser environments.
62-
* @param {string} [options.headers] - The headers to send with the request in browser environments.
6361
* @param {T} options.defaultConfig - The default configuration.
6462
* @returns {Promise<T>} The merged configuration.
6563
* @example ```ts
66-
* // Merges arrays if both configs are arrays, otherwise does object deep merge
6764
* await loadConfig({
6865
* name: 'example',
69-
* endpoint: '/api/my-custom-config/endpoint',
70-
* defaultConfig: [{ foo: 'bar' }]
66+
* defaultConfig: { foo: 'bar' }
7167
* })
7268
* ```
7369
*/
7470
export async function loadConfig<T>({
7571
name = '',
7672
cwd,
7773
defaultConfig,
78-
// configDir,
79-
// generatedDir,
80-
endpoint,
81-
headers = {
82-
'Accept': 'application/json',
83-
'Content-Type': 'application/json',
84-
},
8574
}: Config<T>): Promise<T> {
86-
// If running in a server environment, load the config from the file system
87-
if (typeof window === 'undefined') {
88-
const baseDir = cwd || process.cwd()
89-
const extensions = ['.ts', '.js', '.mjs', '.cjs', '.json']
90-
91-
// Try loading config in order of preference
92-
const configPaths = [
93-
`${name}.config`,
94-
`.${name}.config`,
95-
name,
96-
`.${name}`,
97-
]
98-
99-
for (const configPath of configPaths) {
100-
for (const ext of extensions) {
101-
const fullPath = resolve(baseDir, `${configPath}${ext}`)
102-
const config = await tryLoadConfig(fullPath, defaultConfig)
103-
if (config !== null)
104-
return config
105-
}
75+
// Server environment: load the config from the file system
76+
const baseDir = cwd || process.cwd()
77+
const extensions = ['.ts', '.js', '.mjs', '.cjs', '.json']
78+
79+
// Try loading config in order of preference
80+
const configPaths = [
81+
`${name}.config`,
82+
`.${name}.config`,
83+
name,
84+
`.${name}`,
85+
]
86+
87+
for (const configPath of configPaths) {
88+
for (const ext of extensions) {
89+
const fullPath = resolve(baseDir, `${configPath}${ext}`)
90+
const config = await tryLoadConfig(fullPath, defaultConfig)
91+
if (config !== null)
92+
return config
10693
}
107-
108-
console.error('Failed to load client config from any expected location')
109-
110-
return defaultConfig
111-
}
112-
113-
// Browser environment checks
114-
if (!endpoint) {
115-
console.warn('An API endpoint is required to load the client config.')
116-
117-
return defaultConfig
11894
}
11995

120-
// If running in a browser environment, load the config from an API endpoint
121-
try {
122-
const response = await fetch(endpoint, {
123-
method: 'GET',
124-
headers: {
125-
'Accept': 'application/json',
126-
'Content-Type': 'application/json',
127-
...headers,
128-
},
129-
})
130-
131-
if (!response.ok)
132-
throw new Error(`HTTP error! status: ${response.status}`)
133-
134-
const loadedConfig = await response.json() as T
135-
136-
// Validate that the loaded config can be merged with the default config
137-
try {
138-
return deepMerge(defaultConfig, loadedConfig) as T
139-
}
140-
catch {
141-
return defaultConfig
142-
}
143-
}
144-
catch (error) {
145-
console.error('Failed to load client config:', error)
146-
147-
return defaultConfig
148-
}
96+
console.error('Failed to load client config from any expected location')
97+
return defaultConfig
14998
}
15099

151100
export const defaultConfigDir: string = resolve(

Diff for: ‎test/browser.test.ts

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, expect, it, mock, spyOn } from 'bun:test'
2+
import { loadConfig } from '../src/browser'
3+
4+
describe('browser', () => {
5+
it('should detect browser environment correctly', () => {
6+
// Mock window to simulate browser environment
7+
const originalWindow = globalThis.window
8+
// @ts-expect-error - mocking window
9+
globalThis.window = {}
10+
11+
const mockFetch = mock(() =>
12+
Promise.resolve({
13+
ok: true,
14+
json: () => Promise.resolve({ host: 'api-host' }),
15+
}),
16+
)
17+
// @ts-expect-error - mocking fetch
18+
globalThis.fetch = mockFetch
19+
20+
const defaultConfig = { port: 3000, host: 'localhost' }
21+
const result = loadConfig({
22+
name: 'test-app',
23+
endpoint: '/api/config',
24+
defaultConfig,
25+
})
26+
27+
expect(result).resolves.toEqual({ port: 3000, host: 'api-host' })
28+
29+
// Restore window
30+
globalThis.window = originalWindow
31+
})
32+
33+
it('should handle missing endpoint gracefully', async () => {
34+
// Mock window to simulate browser environment
35+
const originalWindow = globalThis.window
36+
// @ts-expect-error - mocking window
37+
globalThis.window = {}
38+
39+
const consoleSpy = spyOn(console, 'warn')
40+
const defaultConfig = { port: 3000, host: 'localhost' }
41+
const result = await loadConfig({
42+
name: 'test-app',
43+
defaultConfig,
44+
})
45+
46+
expect(result).toEqual(defaultConfig)
47+
expect(consoleSpy).toHaveBeenCalledWith('An API endpoint is required to load the client config.')
48+
49+
// Restore window
50+
globalThis.window = originalWindow
51+
consoleSpy.mockRestore()
52+
})
53+
54+
it('should handle custom headers correctly', async () => {
55+
// Mock window to simulate browser environment
56+
const originalWindow = globalThis.window
57+
// @ts-expect-error - mocking window
58+
globalThis.window = {}
59+
60+
const mockFetch = mock(() =>
61+
Promise.resolve({
62+
ok: true,
63+
json: () => Promise.resolve({ host: 'api-host' }),
64+
}),
65+
)
66+
// @ts-expect-error - mocking fetch
67+
globalThis.fetch = mockFetch
68+
69+
const defaultConfig = { port: 3000, host: 'localhost' }
70+
const customHeaders = {
71+
'Authorization': 'Bearer token',
72+
'X-Custom-Header': 'value',
73+
}
74+
75+
const result = await loadConfig({
76+
name: 'test-app',
77+
endpoint: '/api/config',
78+
headers: customHeaders,
79+
defaultConfig,
80+
})
81+
82+
expect(result).toEqual({ port: 3000, host: 'api-host' })
83+
expect(mockFetch).toHaveBeenCalledWith('/api/config', {
84+
method: 'GET',
85+
headers: {
86+
...customHeaders,
87+
'Accept': 'application/json',
88+
'Content-Type': 'application/json',
89+
},
90+
})
91+
92+
// Restore window
93+
globalThis.window = originalWindow
94+
})
95+
96+
it('should handle invalid JSON response', async () => {
97+
// Mock window to simulate browser environment
98+
const originalWindow = globalThis.window
99+
// @ts-expect-error - mocking window
100+
globalThis.window = {}
101+
102+
const mockFetch = mock(() =>
103+
Promise.resolve({
104+
ok: true,
105+
json: () => Promise.reject(new Error('Invalid JSON')),
106+
}),
107+
)
108+
// @ts-expect-error - mocking fetch
109+
globalThis.fetch = mockFetch
110+
111+
const defaultConfig = { port: 3000, host: 'localhost' }
112+
const result = await loadConfig({
113+
name: 'test-app',
114+
endpoint: '/api/config',
115+
defaultConfig,
116+
})
117+
118+
expect(result).toEqual(defaultConfig)
119+
120+
// Restore window
121+
globalThis.window = originalWindow
122+
})
123+
})

0 commit comments

Comments
 (0)
Please sign in to comment.