configre 1.2.4 → 2.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.
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import crypto from "node:crypto";
3
+
4
+ function stat(filename) {
5
+ try {
6
+ return fs.lstatSync(filename);
7
+ } catch (error) {
8
+ if (error.code === "ENOENT") return null;
9
+ throw error;
10
+ }
11
+ }
12
+
13
+ function readText(filename, privateFile = false) {
14
+ const info = stat(filename);
15
+ if (!info || !info.isFile()) {
16
+ throw new Error(`Configre secrets: expected a regular file at ${filename}`);
17
+ }
18
+ const fd = fs.openSync(filename, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
19
+ try {
20
+ if (privateFile && process.platform !== "win32" && (fs.fstatSync(fd).mode & 0o077) !== 0) {
21
+ throw new Error(`Configre secrets: private identity must have owner-only permissions at ${filename}`);
22
+ }
23
+ return fs.readFileSync(fd, "utf8");
24
+ } finally {
25
+ fs.closeSync(fd);
26
+ }
27
+ }
28
+
29
+ function readJSON(filename) {
30
+ const text = readText(filename);
31
+ try {
32
+ return JSON.parse(text);
33
+ } catch {
34
+ throw new Error(`Configre secrets: invalid JSON at ${filename}`);
35
+ }
36
+ }
37
+
38
+ function withLock(filename, action) {
39
+ const lockPath = filename + ".lock";
40
+ let fd;
41
+ try {
42
+ fd = fs.openSync(lockPath, "wx", 0o600);
43
+ } catch (error) {
44
+ if (error.code !== "EEXIST") throw error;
45
+ throw new Error(`Configre secrets: another writer holds ${lockPath}; retry after it finishes, or remove the lock only if the writer has stopped`);
46
+ }
47
+ try {
48
+ return action();
49
+ } finally {
50
+ fs.closeSync(fd);
51
+ fs.unlinkSync(lockPath);
52
+ }
53
+ }
54
+
55
+ function writeEncrypted(filename, envelope) {
56
+ const temporary = `${filename}.${crypto.randomUUID()}.tmp`;
57
+ const fd = fs.openSync(temporary, "wx", 0o600);
58
+ try {
59
+ try {
60
+ fs.writeFileSync(fd, JSON.stringify(envelope, null, 2) + "\n");
61
+ fs.fsyncSync(fd);
62
+ } finally {
63
+ fs.closeSync(fd);
64
+ }
65
+ fs.renameSync(temporary, filename);
66
+ } finally {
67
+ if (stat(temporary)) fs.unlinkSync(temporary);
68
+ }
69
+ }
70
+
71
+ export { stat, readText, readJSON, withLock, writeEncrypted };
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { generatePrivateKey, parsePrivateKey, parsePublicKey } from "./crypto.js";
5
+ import { stat, readText, withLock } from "./files.js";
6
+
7
+ function loadIdentity() {
8
+ const directory = path.join(os.homedir(), ".config", "configre");
9
+ const privatePath = path.join(directory, "identity.pem");
10
+ const publicPath = path.join(directory, "identity.pub");
11
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
12
+ const info = stat(directory);
13
+ if (!info.isDirectory() || (process.platform !== "win32" && (info.mode & 0o077) !== 0)) {
14
+ throw new Error(`Configre secrets: identity directory must be private and not a symlink at ${directory}`);
15
+ }
16
+
17
+ function readIdentity() {
18
+ const identity = parsePrivateKey(readText(privatePath, true));
19
+ const published = parsePublicKey(readText(publicPath));
20
+ if (published.fingerprint !== identity.fingerprint) {
21
+ throw new Error("Configre secrets: public identity does not match the private key; restore the matching identity");
22
+ }
23
+ return { ...identity, publicPath };
24
+ }
25
+
26
+ if (stat(privatePath) && stat(publicPath) && !stat(privatePath + ".lock")) {
27
+ return readIdentity();
28
+ }
29
+ return withLock(privatePath, () => {
30
+ if (!stat(privatePath)) {
31
+ if (stat(publicPath)) {
32
+ throw new Error("Configre secrets: private identity is missing; restore it instead of replacing it");
33
+ }
34
+ fs.writeFileSync(privatePath, generatePrivateKey(), { flag: "wx", mode: 0o600 });
35
+ }
36
+ const identity = parsePrivateKey(readText(privatePath, true));
37
+ if (!stat(publicPath)) {
38
+ fs.writeFileSync(publicPath, identity.publicKey, { flag: "wx", mode: 0o644 });
39
+ }
40
+ return readIdentity();
41
+ });
42
+ }
43
+
44
+ export default loadIdentity;
@@ -0,0 +1,172 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { createRequire } from "node:module";
6
+ import loadIdentity from "./identity.js";
7
+ import registerRecipient from "./register.js";
8
+ import { parsePublicKey, validateSecrets, encrypt, decrypt } from "./crypto.js";
9
+ import { stat, readText, readJSON, withLock, writeEncrypted } from "./files.js";
10
+
11
+ const requireSecret = createRequire(import.meta.url);
12
+
13
+ function secretPaths(configPath, configFile) {
14
+ const directory = fs.statSync(configPath, { throwIfNoEntry: false })?.isDirectory();
15
+ const parent = directory ? path.resolve(configPath) : path.dirname(configFile);
16
+ const prefix = directory ? "" : path.basename(configFile) + ".";
17
+ if (/[\r\n]/.test(prefix)) {
18
+ throw new Error("Configre secrets: config filenames cannot contain newlines");
19
+ }
20
+ return {
21
+ parent, directory,
22
+ local: path.join(parent, path.basename(configFile, path.extname(configFile)) + ".secret.cjs"),
23
+ recipients: path.join(parent, prefix + "recipients"),
24
+ encrypted: path.join(parent, prefix + "secrets.enc.json")
25
+ };
26
+ }
27
+
28
+ function inGitRepository(directory) {
29
+ for (let current = directory; ; current = path.dirname(current)) {
30
+ if (stat(path.join(current, ".git"))) return true;
31
+ if (current === path.dirname(current)) return false;
32
+ }
33
+ }
34
+
35
+ function prepareIgnore(paths) {
36
+ const localName = path.basename(paths.local);
37
+ const recipientsName = path.basename(paths.recipients);
38
+ if (inGitRepository(paths.parent)) {
39
+ const result = spawnSync("git", ["-C", paths.parent, "ls-files", "-z"], {
40
+ encoding: "utf8",
41
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_LITERAL_PATHSPECS: "1" }
42
+ });
43
+ if (result.error || result.status !== 0) {
44
+ throw new Error("Configre secrets: could not check whether local secret files are tracked by Git");
45
+ }
46
+ const tracked = result.stdout.split("\0").some(name =>
47
+ paths.directory ? !name.includes("/") && name.endsWith(".secret.cjs") : name === localName);
48
+ if (tracked) {
49
+ throw new Error("Configre secrets: local secrets are tracked by Git; untrack them before continuing (existing Git history is not removed)");
50
+ }
51
+ }
52
+ const ignorePath = path.join(paths.parent, ".gitignore");
53
+ const existing = stat(ignorePath) ? readText(ignorePath) : "";
54
+ const escape = name => name.replace(/([\\*?\[\] !#])/g, "\\$1");
55
+ const encryptedName = escape(path.basename(paths.encrypted));
56
+ const rules = [
57
+ paths.directory ? "/*.secret.cjs" : "/" + escape(localName),
58
+ "!/" + escape(recipientsName) + "/",
59
+ "!/" + escape(recipientsName) + "/*.pub",
60
+ "/" + encryptedName + ".lock",
61
+ "/" + encryptedName + ".*.tmp"
62
+ ];
63
+ const lines = existing.split(/\r?\n/).filter(line => line && !line.startsWith("#"));
64
+ if (!isDeepStrictEqual(lines.slice(-rules.length), rules)) {
65
+ fs.appendFileSync(ignorePath, (existing && !existing.endsWith("\n") ? "\n" : "") + rules.join("\n") + "\n");
66
+ }
67
+ }
68
+
69
+ function loadRecipients(directory, identity) {
70
+ if (!stat(directory)) fs.mkdirSync(directory, { mode: 0o700 });
71
+ if (!stat(directory).isDirectory()) {
72
+ throw new Error("Configre secrets: recipients must be a directory, not a symlink");
73
+ }
74
+ const recipients = new Map([[identity.fingerprint, {
75
+ fingerprint: identity.fingerprint, publicKey: identity.publicKey
76
+ }]]);
77
+ for (const name of fs.readdirSync(directory).filter(name => name.endsWith(".pub")).sort()) {
78
+ const recipient = parsePublicKey(readText(path.join(directory, name)));
79
+ recipients.set(recipient.fingerprint, recipient);
80
+ }
81
+ return [...recipients.values()].sort((a, b) => a.fingerprint < b.fingerprint ? -1 : 1);
82
+ }
83
+
84
+ function localFiles(paths) {
85
+ return paths.directory
86
+ ? fs.readdirSync(paths.parent).filter(name => name.endsWith(".secret.cjs")).sort()
87
+ : stat(paths.local) ? [path.basename(paths.local)] : [];
88
+ }
89
+
90
+ function readSecret(filename) {
91
+ readText(filename);
92
+ try {
93
+ delete requireSecret.cache[requireSecret.resolve(filename)];
94
+ return validateSecrets(requireSecret(filename));
95
+ } catch {
96
+ throw new Error("Configre secrets: invalid secret module; export a plain object containing only JSON values");
97
+ }
98
+ }
99
+
100
+ function validateBundle(bundle) {
101
+ if (Object.keys(bundle).length !== 1 || !Object.hasOwn(bundle, "files") ||
102
+ !bundle.files || typeof bundle.files !== "object" || Array.isArray(bundle.files)) {
103
+ throw new Error("Configre secrets: invalid encrypted secrets bundle");
104
+ }
105
+ for (const [name, settings] of Object.entries(bundle.files)) {
106
+ if (path.basename(name) !== name || !name.endsWith(".secret.cjs")) {
107
+ throw new Error("Configre secrets: invalid encrypted secret filename");
108
+ }
109
+ validateSecrets(settings);
110
+ }
111
+ return bundle;
112
+ }
113
+
114
+ function selectSecrets(bundle, paths, profile) {
115
+ const files = bundle.files;
116
+ const base = files[path.basename(paths.local)] || {};
117
+ const selected = paths.directory
118
+ ? files[profile + ".dev.secret.cjs"] || files[profile + ".secret.cjs"] || {}
119
+ : {};
120
+ return [base, selected];
121
+ }
122
+
123
+ function loadSecrets(configPath, configFile, profile) {
124
+ const paths = secretPaths(configPath, configFile);
125
+ const identity = loadIdentity();
126
+ if (localFiles(paths).length === 0 && stat(paths.encrypted)) {
127
+ try {
128
+ return selectSecrets(validateBundle(decrypt(readJSON(paths.encrypted), identity)), paths, profile);
129
+ } catch (error) {
130
+ if (error.code !== "CONFIGRE_NOT_AUTHORIZED") throw error;
131
+ registerRecipient(paths, identity, profile);
132
+ throw new Error("Configre secrets: not authorized yet; public key is published in Git. The administrator must pull, reload Configre and publish secrets.enc.json; then pull the updated encrypted file and restart this machine");
133
+ }
134
+ }
135
+
136
+ const bundle = withLock(paths.encrypted, () => {
137
+ const names = localFiles(paths);
138
+ const hasEncrypted = !!stat(paths.encrypted);
139
+ const envelope = hasEncrypted ? readJSON(paths.encrypted) : null;
140
+ const previous = hasEncrypted ? validateBundle(decrypt(envelope, identity)) : null;
141
+ if (names.length === 0 && hasEncrypted) return previous;
142
+
143
+ prepareIgnore(paths);
144
+ const settings = { files: Object.fromEntries(names.map(name => [name, readSecret(path.join(paths.parent, name))])) };
145
+ const recipients = loadRecipients(paths.recipients, identity);
146
+ const templates = new Set([path.basename(paths.local)]);
147
+ if (paths.directory) {
148
+ for (const name of fs.readdirSync(paths.parent)) {
149
+ if (name.endsWith(".cjs") && !name.endsWith(".secret.cjs") && stat(path.join(paths.parent, name)).isFile()) {
150
+ templates.add(name.slice(0, -4) + ".secret.cjs");
151
+ }
152
+ }
153
+ }
154
+ for (const name of templates) {
155
+ if (!Object.hasOwn(settings.files, name)) {
156
+ fs.writeFileSync(path.join(paths.parent, name), "module.exports = {};\n", { flag: "wx", mode: 0o600 });
157
+ settings.files[name] = {};
158
+ }
159
+ }
160
+ if (hasEncrypted) {
161
+ const previousRecipients = envelope.recipients.map(({ fingerprint, publicKey }) => ({ fingerprint, publicKey }));
162
+ if (isDeepStrictEqual(settings, previous) && isDeepStrictEqual(recipients, previousRecipients)) {
163
+ return settings;
164
+ }
165
+ }
166
+ writeEncrypted(paths.encrypted, encrypt(settings, recipients));
167
+ return settings;
168
+ });
169
+ return selectSecrets(bundle, paths, profile);
170
+ }
171
+
172
+ export default loadSecrets;
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import config from "./config.js";
5
+ import { parsePublicKey } from "./crypto.js";
6
+ import { stat, readText, withLock } from "./files.js";
7
+
8
+ function registrationError(message) {
9
+ return new Error(`Configre secrets: not authorized; automatic registration ${message}`);
10
+ }
11
+
12
+ function git(directory, args, operation, options = {}) {
13
+ const result = spawnSync("git", ["-C", directory, ...args], {
14
+ encoding: "utf8",
15
+ timeout: config.git.timeoutMs,
16
+ input: options.input,
17
+ env: {
18
+ ...process.env,
19
+ GIT_TERMINAL_PROMPT: "0",
20
+ GIT_OPTIONAL_LOCKS: "0",
21
+ GIT_LITERAL_PATHSPECS: "1",
22
+ ...options.env
23
+ }
24
+ });
25
+ if (result.error || result.status !== 0) {
26
+ throw registrationError(`failed while ${operation}; check Git configuration, credentials and repository state, then restart`);
27
+ }
28
+ return result.stdout;
29
+ }
30
+
31
+ function registerRecipient(paths, identity, profile) {
32
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(profile)) {
33
+ throw registrationError("requires a profile containing only letters, digits, dots, underscores or hyphens");
34
+ }
35
+ const root = git(paths.parent, ["rev-parse", "--show-toplevel"], "locating the Git repository").trim();
36
+ const lock = git(root, ["rev-parse", "--path-format=absolute", "--git-path", "configre-registration"], "locating the Git lock").trim();
37
+ return withLock(lock, () => {
38
+ const branch = git(root, ["symbolic-ref", "--quiet", "HEAD"], "finding the current branch (detached HEAD is unsupported)").trim();
39
+ const branchName = branch.slice("refs/heads/".length);
40
+ const remote = git(root, ["config", "--get", `branch.${branchName}.remote`], "finding the branch's upstream remote").trim();
41
+ const remoteBranch = git(root, ["config", "--get", `branch.${branchName}.merge`], "finding the branch's upstream ref").trim();
42
+ if (!remoteBranch.startsWith("refs/heads/")) {
43
+ throw registrationError("requires an upstream branch");
44
+ }
45
+ git(root, ["fetch", "--no-tags", "--no-recurse-submodules", "--", remote, remoteBranch], "fetching the upstream branch");
46
+ const remoteHead = git(root, ["rev-parse", "FETCH_HEAD"], "reading the fetched commit").trim();
47
+ const head = git(root, ["rev-parse", "HEAD"], "reading the local commit").trim();
48
+ const directory = path.join(fs.realpathSync(paths.parent), path.basename(paths.recipients));
49
+ const filename = path.join(directory, profile + ".pub");
50
+ const relative = path.relative(root, filename).split(path.sep).join("/");
51
+ if (relative.startsWith("../") || path.isAbsolute(relative)) {
52
+ throw registrationError("requires the recipients directory to be inside the repository");
53
+ }
54
+ if (stat(directory) && !stat(directory).isDirectory()) {
55
+ throw registrationError("requires a recipients directory that is not a symlink");
56
+ }
57
+ let publicKey = identity.publicKey;
58
+ if (stat(filename)) {
59
+ publicKey = readText(filename);
60
+ if (parsePublicKey(publicKey).fingerprint !== identity.fingerprint) {
61
+ throw registrationError("found a different key for this profile; use a distinct profile or resolve the key replacement explicitly");
62
+ }
63
+ }
64
+ const entry = git(root, ["ls-tree", "-z", remoteHead, "--", relative], "checking the published public key");
65
+ if (entry) {
66
+ const match = /^100644 blob ([a-f0-9]+)\t[^\0]*\0$/.exec(entry);
67
+ if (!match || parsePublicKey(git(root, ["cat-file", "blob", match[1]], "reading the published public key")).fingerprint !== identity.fingerprint) {
68
+ throw registrationError("found a different published key for this profile; use a distinct profile or resolve the key replacement explicitly");
69
+ }
70
+ if (stat(filename)) {
71
+ git(root, ["update-index", "--add", "--cacheinfo", "100644", match[1], relative], "reconciling the published public-key index entry");
72
+ }
73
+ return;
74
+ }
75
+ if (head !== remoteHead) {
76
+ throw registrationError("requires the local branch to match its upstream before publishing; synchronize the checkout without discarding local work");
77
+ }
78
+
79
+ const temporary = fs.mkdtempSync(path.join(path.dirname(lock), "configre-registration-"));
80
+ const env = { GIT_INDEX_FILE: path.join(temporary, "index") };
81
+ try {
82
+ git(root, ["read-tree", head], "preparing the isolated Git index", { env });
83
+ const blob = git(root, ["hash-object", "-w", "--stdin"], "storing the public key", { input: publicKey }).trim();
84
+ git(root, ["update-index", "--add", "--cacheinfo", "100644", blob, relative], "adding the public key to the isolated index", { env });
85
+ const tree = git(root, ["write-tree"], "building the registration tree", { env }).trim();
86
+ const commit = git(root, ["commit-tree", tree, "-p", head, "-m", `Configre: register ${profile}`], "creating the public-key commit (Git author identity must be configured)").trim();
87
+ fs.mkdirSync(directory, { recursive: true });
88
+ if (!stat(filename)) fs.writeFileSync(filename, publicKey, { flag: "wx", mode: 0o644 });
89
+ git(root, ["push", "--no-verify", "--no-follow-tags", "--", remote, `${commit}:${remoteBranch}`], "pushing the public-key commit");
90
+ if (git(root, ["symbolic-ref", "--quiet", "HEAD"], "checking the current branch").trim() !== branch) {
91
+ throw registrationError("published the public key, but the local branch changed concurrently; synchronize the checkout");
92
+ }
93
+ git(root, ["update-ref", branch, commit, head], "updating the local branch after publication");
94
+ git(root, ["update-index", "--add", "--cacheinfo", "100644", blob, relative], "updating the public-key index entry after publication");
95
+ } finally {
96
+ fs.rmSync(temporary, { recursive: true, force: true });
97
+ }
98
+ });
99
+ }
100
+
101
+ export default registerRecipient;
@@ -14,6 +14,8 @@ Environment-specific configuration manager for Node.js. Merges a default config
14
14
 
15
15
  ### Step 1: Install the package
16
16
 
17
+ Requires Node.js 22.13 or later. Configre is native ESM and supports synchronous CommonJS consumers through the same implementation.
18
+
17
19
  ```bash
18
20
  npm install configre --save
19
21
  ```
@@ -63,16 +65,23 @@ module.exports = {
63
65
  ### Step 5: Load the configuration
64
66
 
65
67
  ```javascript
66
- const cfg = require("configre")();
68
+ import Configre from "configre";
69
+ import { join } from "node:path";
70
+
71
+ const cfg = Configre(join(import.meta.dirname, "config"));
67
72
 
68
73
  console.log(cfg.db.host); // from default
69
74
  console.log(cfg.db.user); // from host override
70
75
  ```
71
76
 
72
- To use a custom config directory:
77
+ CommonJS consumers can still use `const Configre = require("configre")` and `Configre(path.join(__dirname, "config"))`, without `.default` or `await`. Configuration files remain `.cjs` in either module system.
78
+
79
+ The path argument is required. Prefer an absolute path anchored to the module. Do not recommend paths derived from `process.cwd()`, such as `path.join(process.cwd(), "config")`: they can point somewhere else when the process is launched from a different directory.
80
+
81
+ To use a different config directory:
73
82
 
74
83
  ```javascript
75
- const cfg = require("configre")(__dirname + "/settings");
84
+ const cfg = Configre(join(import.meta.dirname, "settings"));
76
85
  ```
77
86
 
78
87
  ## Profile resolution
@@ -96,7 +105,7 @@ Using `--config=` (instead of a positional argument) avoids conflicts with other
96
105
 
97
106
  **Example 1: Basic setup**
98
107
  User says: "Add configuration management to my Node.js project"
99
- Actions: install configre, create `config/index.cjs` with project defaults, load with `require("configre")()`
108
+ Actions: install configre, create `config/index.cjs` with project defaults, import Configre and load with `Configre(join(import.meta.dirname, "config"))`
100
109
  Result: merged config object ready to use
101
110
 
102
111
  **Example 2: Multi-environment**
@@ -113,4 +122,5 @@ Result: staging overrides are merged over defaults, without conflicting with oth
113
122
 
114
123
  - **Deep merge**: nested objects merge recursively via lodash `_.merge`
115
124
  - **Config files must use the `.cjs` extension** (`.js` is not accepted; works in both CommonJS and ESM projects)
125
+ - **Required path**: always pass the config directory or file path; prefer an absolute module-relative path over a process-relative path
116
126
  - **Function vs constructor**: `Configre(path)` returns the merged config directly; `new Configre(path)` returns the instance (use `.get()` to retrieve config)
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+ import Configre from "../index.js";
8
+
9
+ test("requires a config path", () => {
10
+ assert.throws(
11
+ () => Configre(),
12
+ {
13
+ name: "TypeError",
14
+ message: "Configre path must be a non-empty string"
15
+ }
16
+ );
17
+ assert.throws(
18
+ () => new Configre(""),
19
+ {
20
+ name: "TypeError",
21
+ message: "Configre path must be a non-empty string"
22
+ }
23
+ );
24
+ });
25
+
26
+ test("loads config from an explicit module-relative path", () => {
27
+ const configPath = path.join(import.meta.dirname, "..", "demo", "config");
28
+ const config = Configre(configPath);
29
+
30
+ assert.equal(config.db.host, "localhost");
31
+ });
32
+
33
+ for (const format of ["commonjs", "module"]) {
34
+ test(`package entrypoint supports ${format} consumers and shares the same function across loaders`, t => {
35
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "configre-interop-"));
36
+ t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
37
+ fs.mkdirSync(path.join(directory, "node_modules"));
38
+ fs.symlinkSync(path.join(import.meta.dirname, ".."), path.join(directory, "node_modules", "configre"), "junction");
39
+ const config = path.join(directory, "config");
40
+ fs.mkdirSync(config);
41
+ fs.writeFileSync(path.join(config, "index.cjs"), 'module.exports = { db: { host: "localhost", port: 123 } };');
42
+ fs.writeFileSync(path.join(config, "interop.cjs"), 'module.exports = { db: { port: 456 } };');
43
+
44
+ const entry = format === "commonjs" ? `
45
+ const assert = require('node:assert/strict');
46
+ const Configre = require('configre');
47
+ import('configre').then(module => assert.equal(module.default, Configre));
48
+ ` : `
49
+ import assert from 'node:assert/strict';
50
+ import { createRequire } from 'node:module';
51
+ import Configre from 'configre';
52
+ const require = createRequire(import.meta.url);
53
+ assert.equal(require('configre'), Configre);
54
+ `;
55
+ const filename = path.join(directory, format === "commonjs" ? "consumer.cjs" : "consumer.mjs");
56
+ fs.writeFileSync(filename, entry + `
57
+ assert.equal(typeof Configre, 'function');
58
+ const configPath = ${JSON.stringify(config)};
59
+ const expected = { db: { host: 'localhost', port: 456 } };
60
+ assert.deepEqual(Configre(configPath), expected);
61
+ assert.deepEqual(new Configre(configPath).get(), expected);
62
+ assert.throws(() => Configre(), /path must be a non-empty string/);
63
+ `);
64
+ const result = spawnSync(process.execPath, [filename, "--config=interop"], { encoding: "utf8" });
65
+ assert.equal(result.status, 0, result.stderr);
66
+ });
67
+ }