@rstackjs/load-config 0.0.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 +226 -0
- package/dist/freshImport.js +111 -0
- package/dist/helpers.d.ts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +117 -0
- package/dist/jiti.d.ts +2 -0
- package/dist/native.d.ts +7 -0
- package/dist/resolve.d.ts +1 -0
- package/dist/types.d.ts +62 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Rspack Contrib
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# @rstackjs/load-config
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://npmjs.com/package/@rstackjs/load-config">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/@rstackjs/load-config?style=flat-square&colorA=564341&colorB=EDED91" alt="npm version" />
|
|
6
|
+
</a>
|
|
7
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="license" />
|
|
8
|
+
<a href="https://npmcharts.com/compare/@rstackjs/load-config?minimal=true"><img src="https://img.shields.io/npm/dm/@rstackjs/load-config.svg?style=flat-square&colorA=564341&colorB=EDED91" alt="downloads" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
A config loading utility for the Rstack ecosystem, designed for loading JavaScript and TypeScript config files for projects like Rspack and Rsbuild.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm add @rstackjs/load-config -D
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
[jiti](https://github.com/unjs/jiti) is an optional peer dependency. Install it only when you use the `jiti` or `auto` loaders:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm add jiti -D
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { loadConfig } from '@rstackjs/load-config';
|
|
29
|
+
|
|
30
|
+
const result = await loadConfig<{ name: string }>({
|
|
31
|
+
cwd: process.cwd(),
|
|
32
|
+
configFileNames: ['my-tool.config.ts', 'my-tool.config.mjs'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log(result.content);
|
|
36
|
+
console.log(result.filePath);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Given a config file:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// my-tool.config.ts
|
|
43
|
+
export default {
|
|
44
|
+
name: 'my-tool',
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`loadConfig` returns:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
type LoadConfigResult<Config = unknown> = {
|
|
52
|
+
content: Config;
|
|
53
|
+
filePath: string | null;
|
|
54
|
+
dependencies: string[];
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If no config file is found, `content` is an empty object, `filePath` is `null`, and `dependencies` is an empty array.
|
|
59
|
+
|
|
60
|
+
## Supported Exports
|
|
61
|
+
|
|
62
|
+
Default object export:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export default {
|
|
66
|
+
name: 'my-tool',
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Function export:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export default ({ mode }) => {
|
|
74
|
+
return { mode };
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Async function export:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
export default async ({ mode }) => {
|
|
82
|
+
return { mode };
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### loadConfig
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
function loadConfig<Config = unknown, Params extends unknown[] = []>(
|
|
92
|
+
options?: LoadConfigOptions<Params>,
|
|
93
|
+
): Promise<LoadConfigResult<Config>>;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Options
|
|
97
|
+
|
|
98
|
+
#### cwd
|
|
99
|
+
|
|
100
|
+
The root directory used to resolve config files.
|
|
101
|
+
|
|
102
|
+
- Type: `string`
|
|
103
|
+
- Default: `process.cwd()`
|
|
104
|
+
|
|
105
|
+
#### path
|
|
106
|
+
|
|
107
|
+
A relative or absolute path to a specific config file.
|
|
108
|
+
|
|
109
|
+
- Type: `string`
|
|
110
|
+
- Default: `undefined`
|
|
111
|
+
|
|
112
|
+
When `path` is provided, the file must exist. Relative paths are resolved from `cwd`.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
await loadConfig({
|
|
116
|
+
path: path.join(import.meta.dirname, 'custom.config.ts'),
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### configFileNames
|
|
121
|
+
|
|
122
|
+
A list of file names to search in `cwd` when `path` is not provided.
|
|
123
|
+
|
|
124
|
+
- Type: `string[]`
|
|
125
|
+
- Default: `[]`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
await loadConfig({
|
|
129
|
+
configFileNames: [
|
|
130
|
+
'tool.config.ts',
|
|
131
|
+
'tool.config.mts',
|
|
132
|
+
'tool.config.js',
|
|
133
|
+
'tool.config.mjs',
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### loader
|
|
139
|
+
|
|
140
|
+
Controls how the config file is loaded.
|
|
141
|
+
|
|
142
|
+
- Type: `'auto' | 'jiti' | 'native'`
|
|
143
|
+
- Default: `'auto'`
|
|
144
|
+
|
|
145
|
+
`auto` uses the native loader when possible and falls back to `jiti`.
|
|
146
|
+
|
|
147
|
+
JavaScript config files (`.js`, `.mjs`, `.cjs`) are always attempted with native dynamic import first; if native import fails and `loader` is not `native`, they fall back to `jiti`.
|
|
148
|
+
|
|
149
|
+
TypeScript config files use the native loader in runtimes with TypeScript support, Bun, or Deno; otherwise they use `jiti`.
|
|
150
|
+
|
|
151
|
+
Set `loader` to `native` to disable the `jiti` fallback.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
await loadConfig({
|
|
155
|
+
path: 'tool.config.ts',
|
|
156
|
+
loader: 'native',
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### exportName
|
|
161
|
+
|
|
162
|
+
The export to read from the config module.
|
|
163
|
+
|
|
164
|
+
- Type: `string | false`
|
|
165
|
+
- Default: `'default'`
|
|
166
|
+
|
|
167
|
+
Use a string to read a named export:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// tool.config.ts
|
|
171
|
+
export const config = {
|
|
172
|
+
name: 'my-tool',
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
await loadConfig({
|
|
178
|
+
path: 'tool.config.ts',
|
|
179
|
+
exportName: 'config',
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Set `exportName` to `false` to execute the config file without reading exports. The returned `content` is an empty object.
|
|
184
|
+
|
|
185
|
+
#### configParams
|
|
186
|
+
|
|
187
|
+
Arguments passed to a function config export.
|
|
188
|
+
|
|
189
|
+
- Type: `Params`
|
|
190
|
+
- Default: `[]`
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// tool.config.ts
|
|
194
|
+
export default ({ mode }: { mode: string }) => ({
|
|
195
|
+
mode,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const result = await loadConfig<{ mode: string }, [{ mode: string }]>({
|
|
201
|
+
path: 'tool.config.ts',
|
|
202
|
+
configParams: [{ mode: 'production' }],
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Config functions may be async, but they must return a config object.
|
|
207
|
+
|
|
208
|
+
#### fresh
|
|
209
|
+
|
|
210
|
+
Bypasses module cache when loading the config.
|
|
211
|
+
|
|
212
|
+
- Type: `boolean`
|
|
213
|
+
- Default: `false`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
await loadConfig({
|
|
217
|
+
path: 'tool.config.mjs',
|
|
218
|
+
fresh: true,
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When using the `native` loader and `fresh` is enabled, `dependencies` contains absolute paths for files imported by the config file.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
[MIT](./LICENSE).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Module } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { MessageChannel } from "node:worker_threads";
|
|
4
|
+
const instanceId = Math.random().toString(36).slice(2);
|
|
5
|
+
const relativeImportRE = /^\.{1,2}(?:\/|\\)/;
|
|
6
|
+
function escapeRegExp(value) {
|
|
7
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8
|
+
}
|
|
9
|
+
function buildQueryName() {
|
|
10
|
+
return `fresh-import-${instanceId}`;
|
|
11
|
+
}
|
|
12
|
+
function buildQueryRE(queryName) {
|
|
13
|
+
return new RegExp(`(?:\\?|&)${escapeRegExp(queryName)}=(\\d+),([^&]+)(?:&|$)`);
|
|
14
|
+
}
|
|
15
|
+
function formatTrackingQuery(queryName, id, context) {
|
|
16
|
+
return `?${queryName}=${id},${context}`;
|
|
17
|
+
}
|
|
18
|
+
function trackResolved(specifier, context, result, queryName, queryRE, onDependency) {
|
|
19
|
+
const isRelativeImport = relativeImportRE.test(specifier);
|
|
20
|
+
if ("builtin" === result.format || !isRelativeImport) return result;
|
|
21
|
+
if (!context.parentURL || queryRE.test(result.url) || !result.url.startsWith("file:")) return result;
|
|
22
|
+
const m = queryRE.exec(context.parentURL);
|
|
23
|
+
if (m) {
|
|
24
|
+
const [, id, contextFile] = m;
|
|
25
|
+
onDependency(contextFile, result.url);
|
|
26
|
+
result.url = result.url.replace(/(\?)|$/, (_n, n1)=>`?${queryName}=${id},${contextFile}${"?" === n1 ? "&" : ""}`);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
var loader_default = 'data:text/javascript,Math.random().toString(36).slice(2);%0Aconst relativeImportRE = /^\\.{1,2}(%3F:\\/|\\\\)/;%0Afunction escapeRegExp(value) {%0A%09return value.replace(/[.*+%3F^${}()|[\\]\\\\]/g, "\\\\$&");%0A}%0A/**%0A* Build the regex that matches the tracking query `%3F<name>=<id>,<context>`%0A* (or the `&<name>=...` form).%0A*/%0Afunction buildQueryRE(queryName) {%0A%09return new RegExp(`(%3F:\\\\%3F|&)${escapeRegExp(queryName)}=(\\\\d+),([^&]+)(%3F:&|$)`);%0A}%0A/**%0A* Shared body of the resolve hook for both the on-thread and off-thread%0A* importers. Given an already-resolved `result`, decides whether it is a tracked%0A* relative file dependency; if so, reports it via `onDependency` and tags the%0A* URL so the query propagates to its own dependencies.%0A*%0A* The sync/async difference between the two hooks lives entirely in the caller%0A* (which awaits `nextResolve` or not); this function performs no I/O. `result`%0A* is mutated in place and returned.%0A*/%0Afunction trackResolved(specifier, context, result, queryName, queryRE, onDependency) {%0A%09const isRelativeImport = relativeImportRE.test(specifier);%0A%09if (result.format === "builtin" || !isRelativeImport) return result;%0A%09if (!context.parentURL || queryRE.test(result.url) || !result.url.startsWith("file:")) return result;%0A%09const m = queryRE.exec(context.parentURL);%0A%09if (m) {%0A%09%09const [, id, contextFile] = m;%0A%09%09onDependency(contextFile, result.url);%0A%09%09result.url = result.url.replace(/(\\%3F)|$/, (_n, n1) => `%3F${queryName}=${id},${contextFile}${n1 === "%3F" %3F "&" : ""}`);%0A%09}%0A%09return result;%0A}%0A//%23endregion%0A//%23region src/off-thread/loader.ts%0Alet port;%0Alet queryName;%0Alet queryRE;%0Aconst initialize = async (data) => {%0A%09port = data.port;%0A%09queryName = data.queryName;%0A%09queryRE = buildQueryRE(queryName);%0A};%0Aconst resolve = async (specifier, context, nextResolve) => {%0A%09return trackResolved(specifier, context, await nextResolve(specifier, context), queryName, queryRE, (ctx, url) => {%0A%09%09port.postMessage({%0A%09%09%09context: ctx,%0A%09%09%09url%0A%09%09});%0A%09});%0A};%0A//%23endregion%0Aexport { initialize, resolve };%0A';
|
|
31
|
+
let nextId$1 = 0;
|
|
32
|
+
function createOffThreadImporter() {
|
|
33
|
+
const queryName = buildQueryName();
|
|
34
|
+
const { port1, port2 } = new MessageChannel();
|
|
35
|
+
Module.register(loader_default, {
|
|
36
|
+
data: {
|
|
37
|
+
port: port2,
|
|
38
|
+
queryName
|
|
39
|
+
},
|
|
40
|
+
transferList: [
|
|
41
|
+
port2
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
port1.unref();
|
|
45
|
+
return {
|
|
46
|
+
async collect (specifier) {
|
|
47
|
+
const id = nextId$1++;
|
|
48
|
+
const depsList = /* @__PURE__ */ new Set();
|
|
49
|
+
const onMessage = (e)=>{
|
|
50
|
+
if (e.context === specifier) depsList.add(e.url);
|
|
51
|
+
};
|
|
52
|
+
port1.on("message", onMessage);
|
|
53
|
+
port1.unref();
|
|
54
|
+
try {
|
|
55
|
+
const result = await import(specifier + formatTrackingQuery(queryName, id, specifier));
|
|
56
|
+
await new Promise((resolve)=>setImmediate(resolve));
|
|
57
|
+
return {
|
|
58
|
+
result,
|
|
59
|
+
dependencies: [
|
|
60
|
+
...depsList
|
|
61
|
+
].filter((url)=>url.startsWith("file:")).map((url)=>fileURLToPath(url))
|
|
62
|
+
};
|
|
63
|
+
} finally{
|
|
64
|
+
port1.off("message", onMessage);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let nextId = 0;
|
|
70
|
+
function createOnThreadImporter() {
|
|
71
|
+
const registry = /* @__PURE__ */ new Map();
|
|
72
|
+
const queryName = buildQueryName();
|
|
73
|
+
const queryRE = buildQueryRE(queryName);
|
|
74
|
+
const resolve = (specifier, context, nextResolve)=>trackResolved(specifier, context, nextResolve(specifier, context), queryName, queryRE, (ctx, url)=>{
|
|
75
|
+
registry.get(ctx)?.add(url);
|
|
76
|
+
});
|
|
77
|
+
Module.registerHooks({
|
|
78
|
+
resolve
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
async collect (specifier) {
|
|
82
|
+
const id = nextId++;
|
|
83
|
+
const depsList = /* @__PURE__ */ new Set();
|
|
84
|
+
registry.set(specifier, depsList);
|
|
85
|
+
try {
|
|
86
|
+
return {
|
|
87
|
+
result: await import(specifier + formatTrackingQuery(queryName, id, specifier)),
|
|
88
|
+
dependencies: [
|
|
89
|
+
...depsList
|
|
90
|
+
].filter((url)=>url.startsWith("file:")).map((url)=>fileURLToPath(url))
|
|
91
|
+
};
|
|
92
|
+
} finally{
|
|
93
|
+
registry.delete(specifier);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createImporter() {
|
|
99
|
+
if (Module.registerHooks) return createOnThreadImporter();
|
|
100
|
+
if (Module.register) return createOffThreadImporter();
|
|
101
|
+
}
|
|
102
|
+
let importer;
|
|
103
|
+
let initialized = false;
|
|
104
|
+
function freshImport(specifier) {
|
|
105
|
+
if (!initialized) {
|
|
106
|
+
importer = createImporter();
|
|
107
|
+
initialized = true;
|
|
108
|
+
}
|
|
109
|
+
return importer?.collect(specifier);
|
|
110
|
+
}
|
|
111
|
+
export { freshImport };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ConfigDefinition, ConfigFunction } from './types.js';
|
|
2
|
+
export declare const isConfigFunction: <Config, Params extends unknown[]>(configExport: ConfigDefinition<Config, Params>) => configExport is ConfigFunction<Config, Params>;
|
|
3
|
+
export declare const getConfigExport: <Config, Params extends unknown[]>(configModule: unknown, exportName: string | false, configPath: string) => ConfigDefinition<Config, Params>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LoadConfigOptions, LoadConfigResult } from './types.js';
|
|
2
|
+
export type { ConfigDefinition, ConfigLoader, LoadConfigOptions, LoadConfigResult, } from './types.js';
|
|
3
|
+
export declare function loadConfig<Config = unknown, Params extends unknown[] = []>({ cwd, path, configFileNames, loader, exportName, configParams, fresh, }?: LoadConfigOptions<Params>): Promise<LoadConfigResult<Config>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import node_fs from "node:fs";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
const canReadExport = (module)=>null !== module && ('object' == typeof module || 'function' == typeof module);
|
|
5
|
+
const isConfigFunction = (configExport)=>'function' == typeof configExport;
|
|
6
|
+
const getConfigExport = (configModule, exportName, configPath)=>{
|
|
7
|
+
if (false === exportName) return {};
|
|
8
|
+
if ('default' === exportName) return canReadExport(configModule) && 'default' in configModule ? configModule.default : configModule;
|
|
9
|
+
if (canReadExport(configModule) && Object.hasOwn(configModule, exportName)) return configModule[exportName];
|
|
10
|
+
throw new Error(`Cannot find export ${exportName} in config file: ${configPath}`);
|
|
11
|
+
};
|
|
12
|
+
const loadWithJiti = async (configPath, exportName, fresh)=>{
|
|
13
|
+
let createJiti;
|
|
14
|
+
try {
|
|
15
|
+
({ createJiti } = await import("jiti"));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error('The "jiti" package is required to load this config. Install it with your package manager.', {
|
|
18
|
+
cause: error
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const jiti = createJiti(configPath, {
|
|
22
|
+
moduleCache: !fresh,
|
|
23
|
+
interopDefault: true,
|
|
24
|
+
nativeModules: [
|
|
25
|
+
"typescript"
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
if ('default' === exportName) return {
|
|
29
|
+
configExport: await jiti.import(configPath, {
|
|
30
|
+
default: true
|
|
31
|
+
}),
|
|
32
|
+
dependencies: []
|
|
33
|
+
};
|
|
34
|
+
const configModule = await jiti.import(configPath);
|
|
35
|
+
return {
|
|
36
|
+
configExport: getConfigExport(configModule, exportName, configPath),
|
|
37
|
+
dependencies: []
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const JS_CONFIG_REGEXP = /\.(?:js|mjs|cjs)$/;
|
|
41
|
+
const tryFreshImport = async (configFileURL)=>{
|
|
42
|
+
try {
|
|
43
|
+
const { freshImport } = await import("./freshImport.js");
|
|
44
|
+
return await freshImport(configFileURL);
|
|
45
|
+
} catch {}
|
|
46
|
+
};
|
|
47
|
+
const loadWithNative = async (configPath, fresh)=>{
|
|
48
|
+
const configFileURL = pathToFileURL(configPath).href;
|
|
49
|
+
if (!fresh) {
|
|
50
|
+
const configModule = await import(configFileURL);
|
|
51
|
+
return {
|
|
52
|
+
configModule,
|
|
53
|
+
dependencies: []
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const freshImportResult = await tryFreshImport(configFileURL);
|
|
57
|
+
if (freshImportResult) return {
|
|
58
|
+
configModule: freshImportResult.result,
|
|
59
|
+
dependencies: freshImportResult.dependencies.sort()
|
|
60
|
+
};
|
|
61
|
+
const configModule = await import(`${configFileURL}?t=${Date.now()}`);
|
|
62
|
+
return {
|
|
63
|
+
configModule,
|
|
64
|
+
dependencies: []
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const resolveConfigPath = (root, customConfig, configFileNames = [])=>{
|
|
68
|
+
if (customConfig) {
|
|
69
|
+
const customConfigPath = isAbsolute(customConfig) ? customConfig : join(root, customConfig);
|
|
70
|
+
if (node_fs.existsSync(customConfigPath)) return customConfigPath;
|
|
71
|
+
throw new Error(`Cannot find config file: ${customConfigPath}`);
|
|
72
|
+
}
|
|
73
|
+
for (const file of configFileNames){
|
|
74
|
+
const configFile = join(root, file);
|
|
75
|
+
if (node_fs.existsSync(configFile)) return configFile;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
async function loadConfig({ cwd = process.cwd(), path, configFileNames = [], loader = 'auto', exportName = 'default', configParams = [], fresh = false } = {}) {
|
|
80
|
+
const configPath = resolveConfigPath(cwd, path, configFileNames);
|
|
81
|
+
if (!configPath) return {
|
|
82
|
+
content: {},
|
|
83
|
+
filePath: configPath,
|
|
84
|
+
dependencies: []
|
|
85
|
+
};
|
|
86
|
+
let loadedConfig;
|
|
87
|
+
const useNative = Boolean('native' === loader || 'auto' === loader && (process.features.typescript || process.versions.bun || process.versions.deno));
|
|
88
|
+
if (useNative || JS_CONFIG_REGEXP.test(configPath)) {
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = await loadWithNative(configPath, fresh);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if ('native' === loader) throw err;
|
|
94
|
+
}
|
|
95
|
+
if (result) loadedConfig = {
|
|
96
|
+
configExport: getConfigExport(result.configModule, exportName, configPath),
|
|
97
|
+
dependencies: result.dependencies
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!loadedConfig) loadedConfig = await loadWithJiti(configPath, exportName, fresh);
|
|
101
|
+
const { configExport, dependencies } = loadedConfig;
|
|
102
|
+
if (isConfigFunction(configExport)) {
|
|
103
|
+
const result = await configExport(...configParams);
|
|
104
|
+
if (void 0 === result) throw new Error('The config function must return a config object.');
|
|
105
|
+
return {
|
|
106
|
+
content: result,
|
|
107
|
+
filePath: configPath,
|
|
108
|
+
dependencies
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
content: configExport,
|
|
113
|
+
filePath: configPath,
|
|
114
|
+
dependencies
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export { loadConfig };
|
package/dist/jiti.d.ts
ADDED
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const resolveConfigPath: (root: string, customConfig?: string, configFileNames?: string[]) => string | null;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type ConfigLoader = 'auto' | 'jiti' | 'native';
|
|
2
|
+
export type ConfigFunction<Config, Params extends unknown[]> = (...params: Params) => Config | Promise<Config>;
|
|
3
|
+
export type ConfigDefinition<Config, Params extends unknown[]> = Config | ConfigFunction<Config, Params>;
|
|
4
|
+
export type LoadConfigOptions<Params extends unknown[] = []> = {
|
|
5
|
+
/**
|
|
6
|
+
* The root path to resolve the config file.
|
|
7
|
+
* @default process.cwd()
|
|
8
|
+
*/
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The path to the config file, can be a relative or absolute path.
|
|
12
|
+
* If `path` is not provided, the function will search for the config file in the `cwd`.
|
|
13
|
+
*/
|
|
14
|
+
path?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Config file names to search in `cwd` when `path` is not provided.
|
|
17
|
+
* The package-level loader has no built-in framework defaults.
|
|
18
|
+
* @default []
|
|
19
|
+
*/
|
|
20
|
+
configFileNames?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Specify the config loader, can be `auto`, `jiti` or `native`.
|
|
23
|
+
* @default 'auto'
|
|
24
|
+
*/
|
|
25
|
+
loader?: ConfigLoader;
|
|
26
|
+
/**
|
|
27
|
+
* The export name to read from the config file.
|
|
28
|
+
* Set to `false` to execute the config file without reading exports.
|
|
29
|
+
* @default 'default'
|
|
30
|
+
*/
|
|
31
|
+
exportName?: string | false;
|
|
32
|
+
/**
|
|
33
|
+
* Arguments passed to a function config export.
|
|
34
|
+
* @default []
|
|
35
|
+
*/
|
|
36
|
+
configParams?: Params;
|
|
37
|
+
/**
|
|
38
|
+
* Whether to bypass module cache when loading the config.
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
fresh?: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type LoadConfigResult<Config = unknown> = {
|
|
44
|
+
/**
|
|
45
|
+
* The loaded configuration object.
|
|
46
|
+
*/
|
|
47
|
+
content: Config;
|
|
48
|
+
/**
|
|
49
|
+
* The path to the loaded configuration file.
|
|
50
|
+
* Return `null` if the configuration file is not found.
|
|
51
|
+
*/
|
|
52
|
+
filePath: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Absolute file paths of statically imported (relative) dependencies of the
|
|
55
|
+
* config file.
|
|
56
|
+
*/
|
|
57
|
+
dependencies: string[];
|
|
58
|
+
};
|
|
59
|
+
export type LoadedConfig<Config, Params extends unknown[]> = {
|
|
60
|
+
configExport: ConfigDefinition<Config, Params>;
|
|
61
|
+
dependencies: string[];
|
|
62
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rstackjs/load-config",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"repository": "https://github.com/rstackjs/load-config",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "rs lib",
|
|
19
|
+
"dev": "rs lib -w",
|
|
20
|
+
"lint": "rs lint && prettier -c .",
|
|
21
|
+
"lint:write": "rs lint --fix && prettier -w .",
|
|
22
|
+
"test": "rs test",
|
|
23
|
+
"bump": "pnpx bumpp"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^24.13.2",
|
|
27
|
+
"fresh-import": "^0.2.1",
|
|
28
|
+
"jiti": "^2.7.0",
|
|
29
|
+
"prettier": "^3.9.4",
|
|
30
|
+
"rstack": "0.0.2",
|
|
31
|
+
"typescript": "^6.0.3"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"jiti": "^2.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"jiti": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"packageManager": "pnpm@11.9.0",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
}
|
|
46
|
+
}
|