@jondotsoy/configs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/config.types.d.ts +177 -0
- package/dist/configs.d.ts +21 -0
- package/dist/configs.js +847 -0
- package/dist/errors.d.ts +2 -0
- package/dist/sources/env.d.ts +36 -0
- package/dist/sources/env.js +190 -0
- package/dist/sources/fetch.d.ts +15 -0
- package/dist/sources/fetch.js +182 -0
- package/dist/sources/file.d.ts +28 -0
- package/dist/sources/file.js +278 -0
- package/dist/sources/literal.d.ts +3 -0
- package/dist/sources/literal.js +149 -0
- package/dist/sources/source.d.ts +24 -0
- package/dist/sources/sse.d.ts +22 -0
- package/dist/sources/sse.js +236 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/schema.d.ts +1 -0
- package/dist/types/source.d.ts +19 -0
- package/dist/utils/data-types.d.ts +20 -0
- package/dist/utils/dotenv.d.ts +13 -0
- package/dist/utils/store.d.ts +44 -0
- package/package.json +87 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Source } from "./source";
|
|
2
|
+
/** Maps an env var key to a path into the config tree, e.g. `mapKey("FOO_TAR")`. */
|
|
3
|
+
export type EnvKeyMapper = (key: string) => string[];
|
|
4
|
+
export interface EnvSourceOptions {
|
|
5
|
+
/** The env vars to read. Defaults to `process.env`. */
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
7
|
+
/** Only keys starting with `prefix` are included; the prefix is stripped before `mapKey` runs. */
|
|
8
|
+
prefix?: string;
|
|
9
|
+
/** Only keys ending with `suffix` are included; the suffix is stripped before `mapKey` runs. */
|
|
10
|
+
suffix?: string;
|
|
11
|
+
/** Maps each key to a path. Defaults to the identity mapping: `"FOO_TAR" => ["FOO_TAR"]`. */
|
|
12
|
+
mapKey?: EnvKeyMapper;
|
|
13
|
+
}
|
|
14
|
+
/** Built-in `mapKey` strategies for `EnvSourceOptions.mapKey`, each a factory returning an `EnvKeyMapper`. */
|
|
15
|
+
export declare const mapKey: {
|
|
16
|
+
/**
|
|
17
|
+
* Splits an env key into a lowercase nested path on `separator` (default `"_"`):
|
|
18
|
+
* `"FOO_TAR" => ["foo", "tar"]`. Pass a different `separator` (e.g. `"__"`) to keep a single
|
|
19
|
+
* underscore inside a segment from splitting it, e.g. `"API_KEY_V2"` with `separator: "__"`.
|
|
20
|
+
*/
|
|
21
|
+
snakeCase(options?: {
|
|
22
|
+
separator?: string;
|
|
23
|
+
}): EnvKeyMapper;
|
|
24
|
+
/** Passes each key through unchanged, as a single-segment path: `"FOO_TAR" => ["FOO_TAR"]`. Equivalent to the default when `mapKey` is omitted — spells it out explicitly. */
|
|
25
|
+
identity(): EnvKeyMapper;
|
|
26
|
+
/** Maps an env key to a single camelCase segment: `"FOO_TAR" => ["fooTar"]`. */
|
|
27
|
+
camelCase(): EnvKeyMapper;
|
|
28
|
+
/**
|
|
29
|
+
* Maps specific env keys to explicit paths via a `table` lookup, e.g.
|
|
30
|
+
* `mapKey.lookup({ PORT: ["server", "port"] })` maps `"PORT" => ["server", "port"]`. A key not
|
|
31
|
+
* found in `table` falls back to `fallback` (default: the identity mapping, `key => [key]`).
|
|
32
|
+
*/
|
|
33
|
+
lookup(table: Record<string, string[]>, fallback?: EnvKeyMapper): EnvKeyMapper;
|
|
34
|
+
};
|
|
35
|
+
/** A `Source` that snapshots `env` into a config tree, one field per key (as mapped by `mapKey`). */
|
|
36
|
+
export declare function envSource(options?: EnvSourceOptions): Source<Record<string, unknown>>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/utils/store.ts
|
|
2
|
+
class Store {
|
|
3
|
+
value;
|
|
4
|
+
subscribers = new Set;
|
|
5
|
+
cleanups = new Map;
|
|
6
|
+
mountListeners = new Set;
|
|
7
|
+
unmountListeners = new Set;
|
|
8
|
+
constructor(initial) {
|
|
9
|
+
this.value = initial;
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.value;
|
|
13
|
+
}
|
|
14
|
+
set(next) {
|
|
15
|
+
this.value = next;
|
|
16
|
+
for (const subscriber of this.subscribers) {
|
|
17
|
+
this.runSubscriber(subscriber, this.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
subscribe(subscriber) {
|
|
21
|
+
this.addSubscriber(subscriber);
|
|
22
|
+
this.runSubscriber(subscriber, this.value);
|
|
23
|
+
return () => this.removeSubscriber(subscriber);
|
|
24
|
+
}
|
|
25
|
+
listen(subscriber) {
|
|
26
|
+
this.addSubscriber(subscriber);
|
|
27
|
+
return () => this.removeSubscriber(subscriber);
|
|
28
|
+
}
|
|
29
|
+
runSubscriber(subscriber, value) {
|
|
30
|
+
const cleanup = subscriber(value);
|
|
31
|
+
if (typeof cleanup === "function")
|
|
32
|
+
this.cleanups.set(subscriber, cleanup);
|
|
33
|
+
}
|
|
34
|
+
onMount(listener) {
|
|
35
|
+
this.mountListeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
this.mountListeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
onUnmount(listener) {
|
|
41
|
+
this.unmountListeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.unmountListeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
addSubscriber(subscriber) {
|
|
47
|
+
const wasEmpty = this.subscribers.size === 0;
|
|
48
|
+
if (wasEmpty) {
|
|
49
|
+
for (const listener of this.mountListeners)
|
|
50
|
+
listener();
|
|
51
|
+
}
|
|
52
|
+
this.subscribers.add(subscriber);
|
|
53
|
+
}
|
|
54
|
+
removeSubscriber(subscriber) {
|
|
55
|
+
if (!this.subscribers.delete(subscriber))
|
|
56
|
+
return;
|
|
57
|
+
const cleanup = this.cleanups.get(subscriber);
|
|
58
|
+
this.cleanups.delete(subscriber);
|
|
59
|
+
cleanup?.();
|
|
60
|
+
if (this.subscribers.size === 0) {
|
|
61
|
+
for (const listener of this.unmountListeners)
|
|
62
|
+
listener();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
static onMount(store, callback) {
|
|
66
|
+
let cleanup;
|
|
67
|
+
const unsubMount = store.onMount(() => {
|
|
68
|
+
cleanup = callback();
|
|
69
|
+
});
|
|
70
|
+
const unsubUnmount = store.onUnmount(() => {
|
|
71
|
+
cleanup?.();
|
|
72
|
+
cleanup = undefined;
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
cleanup?.();
|
|
76
|
+
cleanup = undefined;
|
|
77
|
+
unsubMount();
|
|
78
|
+
unsubUnmount();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function create(initial) {
|
|
83
|
+
return new Store(initial);
|
|
84
|
+
}
|
|
85
|
+
function computed(source, selector) {
|
|
86
|
+
const result = new Store(selector(source.get()));
|
|
87
|
+
Store.onMount(result, () => {
|
|
88
|
+
return source.subscribe((value) => {
|
|
89
|
+
result.set(selector(value));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
var store = { create, computed, onMount: Store.onMount };
|
|
95
|
+
|
|
96
|
+
// src/sources/source.ts
|
|
97
|
+
class Source {
|
|
98
|
+
underlying;
|
|
99
|
+
store = new Store(null);
|
|
100
|
+
closed = false;
|
|
101
|
+
closePromise;
|
|
102
|
+
startPromise;
|
|
103
|
+
constructor(underlying) {
|
|
104
|
+
this.underlying = underlying;
|
|
105
|
+
const control = {
|
|
106
|
+
set: (value) => {
|
|
107
|
+
if (this.closed)
|
|
108
|
+
return;
|
|
109
|
+
const next = this.underlying.reduce ? this.underlying.reduce(value, this.store.get()) : value;
|
|
110
|
+
this.store.set(next);
|
|
111
|
+
},
|
|
112
|
+
close: () => {
|
|
113
|
+
this.close();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.startPromise = Promise.resolve().then(() => underlying.start(control)).then(() => {
|
|
117
|
+
return;
|
|
118
|
+
});
|
|
119
|
+
this.startPromise.catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
async open() {
|
|
122
|
+
await this.startPromise;
|
|
123
|
+
return this.store;
|
|
124
|
+
}
|
|
125
|
+
close() {
|
|
126
|
+
if (!this.closePromise) {
|
|
127
|
+
this.closed = true;
|
|
128
|
+
this.closePromise = Promise.resolve(this.underlying.close?.()).then(() => {
|
|
129
|
+
return;
|
|
130
|
+
}, () => {
|
|
131
|
+
return;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return this.closePromise;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/sources/env.ts
|
|
139
|
+
var mapKey = {
|
|
140
|
+
snakeCase(options = {}) {
|
|
141
|
+
const separator = options.separator ?? "_";
|
|
142
|
+
return (key) => key.toLowerCase().split(separator);
|
|
143
|
+
},
|
|
144
|
+
identity() {
|
|
145
|
+
return (key) => [key];
|
|
146
|
+
},
|
|
147
|
+
camelCase() {
|
|
148
|
+
return (key) => [key.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase())];
|
|
149
|
+
},
|
|
150
|
+
lookup(table, fallback = (key) => [key]) {
|
|
151
|
+
return (key) => table[key] ?? fallback(key);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
function setPath(target, path, value) {
|
|
155
|
+
let node = target;
|
|
156
|
+
for (const segment of path.slice(0, -1)) {
|
|
157
|
+
const next = node[segment];
|
|
158
|
+
if (typeof next !== "object" || next === null) {
|
|
159
|
+
node[segment] = {};
|
|
160
|
+
}
|
|
161
|
+
node = node[segment];
|
|
162
|
+
}
|
|
163
|
+
node[path[path.length - 1]] = value;
|
|
164
|
+
}
|
|
165
|
+
function envSource(options = {}) {
|
|
166
|
+
const env = options.env ?? process.env;
|
|
167
|
+
const { prefix, suffix } = options;
|
|
168
|
+
const mapKey2 = options.mapKey ?? ((key) => [key]);
|
|
169
|
+
return new Source({
|
|
170
|
+
start(control) {
|
|
171
|
+
const tree = {};
|
|
172
|
+
for (const [key, value] of Object.entries(env)) {
|
|
173
|
+
if (value === undefined)
|
|
174
|
+
continue;
|
|
175
|
+
if (prefix !== undefined && !key.startsWith(prefix))
|
|
176
|
+
continue;
|
|
177
|
+
if (suffix !== undefined && !key.endsWith(suffix))
|
|
178
|
+
continue;
|
|
179
|
+
const trimmedKey = key.slice(prefix !== undefined ? prefix.length : 0, suffix !== undefined ? key.length - suffix.length : key.length);
|
|
180
|
+
setPath(tree, mapKey2(trimmedKey), value);
|
|
181
|
+
}
|
|
182
|
+
control.set(tree);
|
|
183
|
+
control.close();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
export {
|
|
188
|
+
mapKey,
|
|
189
|
+
envSource
|
|
190
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Source } from "./source";
|
|
2
|
+
export interface FetchSourceOptions {
|
|
3
|
+
url: string | URL;
|
|
4
|
+
method?: string;
|
|
5
|
+
headers?: Bun.HeadersInit;
|
|
6
|
+
/** Attempts to download the data before giving up. Defaults to 1 (no retry). */
|
|
7
|
+
attempts?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A `Source` that fetches a JSON snapshot from `url`. Only JSON is supported: the response is
|
|
11
|
+
* parsed as JSON regardless of what `Content-Type` reports (a non-JSON content type is a fallback
|
|
12
|
+
* attempt, not a hard failure). If the download never succeeds, or the body isn't valid JSON, this
|
|
13
|
+
* logs a `console.error` and leaves the store empty (`null`) instead of throwing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchSource<T = unknown>(options: FetchSourceOptions): Source<T>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// src/utils/store.ts
|
|
2
|
+
class Store {
|
|
3
|
+
value;
|
|
4
|
+
subscribers = new Set;
|
|
5
|
+
cleanups = new Map;
|
|
6
|
+
mountListeners = new Set;
|
|
7
|
+
unmountListeners = new Set;
|
|
8
|
+
constructor(initial) {
|
|
9
|
+
this.value = initial;
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.value;
|
|
13
|
+
}
|
|
14
|
+
set(next) {
|
|
15
|
+
this.value = next;
|
|
16
|
+
for (const subscriber of this.subscribers) {
|
|
17
|
+
this.runSubscriber(subscriber, this.value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
subscribe(subscriber) {
|
|
21
|
+
this.addSubscriber(subscriber);
|
|
22
|
+
this.runSubscriber(subscriber, this.value);
|
|
23
|
+
return () => this.removeSubscriber(subscriber);
|
|
24
|
+
}
|
|
25
|
+
listen(subscriber) {
|
|
26
|
+
this.addSubscriber(subscriber);
|
|
27
|
+
return () => this.removeSubscriber(subscriber);
|
|
28
|
+
}
|
|
29
|
+
runSubscriber(subscriber, value) {
|
|
30
|
+
const cleanup = subscriber(value);
|
|
31
|
+
if (typeof cleanup === "function")
|
|
32
|
+
this.cleanups.set(subscriber, cleanup);
|
|
33
|
+
}
|
|
34
|
+
onMount(listener) {
|
|
35
|
+
this.mountListeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
this.mountListeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
onUnmount(listener) {
|
|
41
|
+
this.unmountListeners.add(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
this.unmountListeners.delete(listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
addSubscriber(subscriber) {
|
|
47
|
+
const wasEmpty = this.subscribers.size === 0;
|
|
48
|
+
if (wasEmpty) {
|
|
49
|
+
for (const listener of this.mountListeners)
|
|
50
|
+
listener();
|
|
51
|
+
}
|
|
52
|
+
this.subscribers.add(subscriber);
|
|
53
|
+
}
|
|
54
|
+
removeSubscriber(subscriber) {
|
|
55
|
+
if (!this.subscribers.delete(subscriber))
|
|
56
|
+
return;
|
|
57
|
+
const cleanup = this.cleanups.get(subscriber);
|
|
58
|
+
this.cleanups.delete(subscriber);
|
|
59
|
+
cleanup?.();
|
|
60
|
+
if (this.subscribers.size === 0) {
|
|
61
|
+
for (const listener of this.unmountListeners)
|
|
62
|
+
listener();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
static onMount(store, callback) {
|
|
66
|
+
let cleanup;
|
|
67
|
+
const unsubMount = store.onMount(() => {
|
|
68
|
+
cleanup = callback();
|
|
69
|
+
});
|
|
70
|
+
const unsubUnmount = store.onUnmount(() => {
|
|
71
|
+
cleanup?.();
|
|
72
|
+
cleanup = undefined;
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
cleanup?.();
|
|
76
|
+
cleanup = undefined;
|
|
77
|
+
unsubMount();
|
|
78
|
+
unsubUnmount();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function create(initial) {
|
|
83
|
+
return new Store(initial);
|
|
84
|
+
}
|
|
85
|
+
function computed(source, selector) {
|
|
86
|
+
const result = new Store(selector(source.get()));
|
|
87
|
+
Store.onMount(result, () => {
|
|
88
|
+
return source.subscribe((value) => {
|
|
89
|
+
result.set(selector(value));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
var store = { create, computed, onMount: Store.onMount };
|
|
95
|
+
|
|
96
|
+
// src/sources/source.ts
|
|
97
|
+
class Source {
|
|
98
|
+
underlying;
|
|
99
|
+
store = new Store(null);
|
|
100
|
+
closed = false;
|
|
101
|
+
closePromise;
|
|
102
|
+
startPromise;
|
|
103
|
+
constructor(underlying) {
|
|
104
|
+
this.underlying = underlying;
|
|
105
|
+
const control = {
|
|
106
|
+
set: (value) => {
|
|
107
|
+
if (this.closed)
|
|
108
|
+
return;
|
|
109
|
+
const next = this.underlying.reduce ? this.underlying.reduce(value, this.store.get()) : value;
|
|
110
|
+
this.store.set(next);
|
|
111
|
+
},
|
|
112
|
+
close: () => {
|
|
113
|
+
this.close();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
this.startPromise = Promise.resolve().then(() => underlying.start(control)).then(() => {
|
|
117
|
+
return;
|
|
118
|
+
});
|
|
119
|
+
this.startPromise.catch(() => {});
|
|
120
|
+
}
|
|
121
|
+
async open() {
|
|
122
|
+
await this.startPromise;
|
|
123
|
+
return this.store;
|
|
124
|
+
}
|
|
125
|
+
close() {
|
|
126
|
+
if (!this.closePromise) {
|
|
127
|
+
this.closed = true;
|
|
128
|
+
this.closePromise = Promise.resolve(this.underlying.close?.()).then(() => {
|
|
129
|
+
return;
|
|
130
|
+
}, () => {
|
|
131
|
+
return;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return this.closePromise;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/sources/fetch.ts
|
|
139
|
+
async function download(url, init) {
|
|
140
|
+
const response = await fetch(url, init);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`fetchSource: received ${response.status} ${response.statusText} from "${url}"`);
|
|
143
|
+
}
|
|
144
|
+
return response;
|
|
145
|
+
}
|
|
146
|
+
function fetchSource(options) {
|
|
147
|
+
const { url, method = "GET", headers, attempts = 1 } = options;
|
|
148
|
+
return new Source({
|
|
149
|
+
async start(control) {
|
|
150
|
+
let response;
|
|
151
|
+
let lastError;
|
|
152
|
+
for (let attempt = 0;attempt < Math.max(1, attempts); attempt++) {
|
|
153
|
+
try {
|
|
154
|
+
response = await download(url, { method, headers });
|
|
155
|
+
break;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
lastError = error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!response) {
|
|
161
|
+
console.error(`fetchSource: failed to fetch "${url}" after ${attempts} attempt(s)`, lastError);
|
|
162
|
+
control.close();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
let data;
|
|
168
|
+
try {
|
|
169
|
+
data = JSON.parse(text);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`fetchSource: response body from "${url}" is not valid JSON (content-type: "${contentType}")`, error);
|
|
172
|
+
control.close();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
control.set(data);
|
|
176
|
+
control.close();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
fetchSource
|
|
182
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Source } from "./source";
|
|
2
|
+
export interface FileSourceOptions {
|
|
3
|
+
/** Republishes the config tree whenever the file changes on disk. Defaults to `true`. */
|
|
4
|
+
watch?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Selects a subtree of the parsed file to use as the config tree, e.g. `["containers", "settings"]`
|
|
7
|
+
* to use `{ containers: { settings: {...} } }`'s inner object and ignore the rest of the file.
|
|
8
|
+
* Defaults to `[]`: the whole parsed file is used, unchanged.
|
|
9
|
+
*/
|
|
10
|
+
treePath?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A `Source` that reads a config tree from a local file — `.json` or `.env` (matched by
|
|
14
|
+
* `path`'s extension, or by the bare `.env` filename itself). `path` may be a plain string or a
|
|
15
|
+
* `file:` `URL` (e.g. `import.meta.resolve(...)` or `new URL("./config.json", import.meta.url)`).
|
|
16
|
+
* Like `fetchSource` and `sseSource`, a read or parse failure is logged via `console.error` and
|
|
17
|
+
* leaves the store empty (`null`) instead of throwing.
|
|
18
|
+
*
|
|
19
|
+
* `treePath` selects a subtree of the parsed file to use, instead of the whole thing. A missing
|
|
20
|
+
* or non-object segment along the way is logged via `console.error`, same as a parse failure —
|
|
21
|
+
* but unlike one, it still publishes a snapshot: an empty object (`{}`), not `null` / the last
|
|
22
|
+
* good value, since the file itself was read and parsed fine.
|
|
23
|
+
*
|
|
24
|
+
* With `watch` (the default), the file is re-read on every change and the store updated live; a
|
|
25
|
+
* parse error on one of those later reads is logged but keeps the last good value, same as
|
|
26
|
+
* `sseSource` does for a bad SSE message. `watch: false` reads the file once and closes.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fileSource<T = unknown>(path: string | URL, options?: FileSourceOptions): Source<T>;
|