@cldmv/wisp 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CLDMV Inc.
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,153 @@
1
+ # @cldmv/wisp
2
+
3
+ A Node.js module for version-agnostic JSON importing, providing transparent support for modern and legacy import syntaxes with automatic fallbacks.
4
+
5
+ ## Overview
6
+
7
+ `@cldmv/wisp` allows you to load JSON files in Node.js without worrying about version-specific import syntax. It automatically tries the most modern import methods first and falls back to reliable file system operations.
8
+
9
+ ## Node.js Version Support
10
+
11
+ | Node Version | `import ... with { type: 'json' }` | `import ... assert { type: 'json' }` | Fallback |
12
+ | ------------ | ---------------------------------- | ------------------------------------ | -------- |
13
+ | ≥ 22.10 | ✅ | ✅ | ✅ |
14
+ | ≥ 20.10 | ✅ | ✅ | ✅ |
15
+ | ≥ 18.20 | ✅ | ✅ | ✅ |
16
+ | ≥ 16.14 | ❌ | ✅ | ✅ |
17
+ | < 16.14 | ❌ | ❌ | ✅ |
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @cldmv/wisp
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### ESM (Modern)
28
+
29
+ ```javascript
30
+ import { wisp, wispSync } from "@cldmv/wisp";
31
+
32
+ // Asynchronous loading
33
+ const config = await wisp("./config.json");
34
+
35
+ // Synchronous loading
36
+ const data = wispSync("./data.json");
37
+ ```
38
+
39
+ ### CJS (CommonJS)
40
+
41
+ ```javascript
42
+ const { wisp, wispSync } = require("@cldmv/wisp");
43
+
44
+ // Asynchronous loading
45
+ wisp("./config.json").then((config) => {
46
+ console.log(config);
47
+ });
48
+
49
+ // Synchronous loading
50
+ const data = wispSync("./data.json");
51
+ ```
52
+
53
+ ## API Reference
54
+
55
+ ### `wisp(input, options?)`
56
+
57
+ Asynchronously loads JSON from a file.
58
+
59
+ #### Parameters
60
+
61
+ - `input` (string | URL): Path or URL to the JSON file
62
+ - `options` (object, optional):
63
+ - `base` (string | URL, optional): Base URL for resolving relative paths. Defaults to the caller's file URL.
64
+ - `validate` (function, optional): Validation function called with the parsed JSON. Throws if validation fails.
65
+ - `reviver` (function, optional): Reviver function passed to `JSON.parse`.
66
+
67
+ #### Returns
68
+
69
+ `Promise<*>`: The parsed JSON value.
70
+
71
+ #### Example
72
+
73
+ ```javascript
74
+ import { wisp } from "@cldmv/wisp";
75
+
76
+ const data = await wisp("./config.json", {
77
+ validate: (json) => {
78
+ if (!json.requiredField) throw new Error("Missing required field");
79
+ },
80
+ reviver: (key, value) => (key === "date" ? new Date(value) : value)
81
+ });
82
+ ```
83
+
84
+ ### `wispSync(input, options?)`
85
+
86
+ Synchronously loads JSON from a file.
87
+
88
+ #### Parameters
89
+
90
+ - `input` (string | URL): Path or URL to the JSON file
91
+ - `options` (object, optional): Same as `wisp` options.
92
+
93
+ #### Returns
94
+
95
+ `Promise<*>`: The parsed JSON value.
96
+
97
+ #### Example
98
+
99
+ ```javascript
100
+ import { wispSync } from "@cldmv/wisp";
101
+
102
+ const data = wispSync("./config.json", {
103
+ validate: (json) => {
104
+ if (!json.version) throw new Error("Version required");
105
+ }
106
+ });
107
+ ```
108
+
109
+ ## Options
110
+
111
+ | Option | Type | Description |
112
+ | ---------- | ---------- | ------------------------------------------------------------------------ |
113
+ | `base` | string/URL | Base URL for relative path resolution. Defaults to caller's file URL. |
114
+ | `validate` | function | Validation function. Receives parsed JSON, should throw on invalid data. |
115
+ | `reviver` | function | JSON.parse reviver function for custom parsing. |
116
+
117
+ ## Path Resolution
118
+
119
+ `@cldmv/wisp` uses caller-aware path resolution:
120
+
121
+ - Relative paths are resolved relative to the file that calls `wisp` or `wispSync`
122
+ - Absolute paths and URLs are used as-is
123
+ - The `base` option overrides the default caller-based resolution
124
+
125
+ ## Fallback Order
126
+
127
+ The module attempts to load JSON in this order:
128
+
129
+ 1. `import(url, { with: { type: 'json' } })` (Node ≥ 18.20/20.10/22)
130
+ 2. `import(url, { assert: { type: 'json' } })` (Node ≥ 16.14)
131
+ 3. `fs.readFile` / `fs.readFileSync` (all supported Node versions)
132
+
133
+ This ensures maximum compatibility across Node.js versions.
134
+
135
+ ## Error Handling
136
+
137
+ Validation errors are prefixed with `@cldmv/wisp:` for easy identification:
138
+
139
+ ```javascript
140
+ try {
141
+ await wisp("./invalid.json", {
142
+ validate: () => {
143
+ throw new Error("Custom validation failed");
144
+ }
145
+ });
146
+ } catch (error) {
147
+ console.log(error.message); // "@cldmv/wisp: Custom validation failed"
148
+ }
149
+ ```
150
+
151
+ ## License
152
+
153
+ MIT © CLDMV Inc.
package/index.cjs ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview CJS entry point for @cldmv/wisp, providing version-agnostic JSON importing.
3
+ * @module @cldmv/wisp
4
+ * @public
5
+ *
6
+ * @description
7
+ * This module provides CommonJS exports for the wisp and wispSync functions.
8
+ * It uses createRequire to load the ESM implementation and re-exports it.
9
+ *
10
+ * @example
11
+ * const { wisp, wispSync } = require('@cldmv/wisp');
12
+ * const data = wispSync('./data.json');
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const { createRequire } = require("module");
18
+ const requireESM = createRequire(__filename);
19
+ const esm = requireESM("./index.mjs");
20
+
21
+ module.exports = esm.default;
22
+ module.exports.default = esm.default;
23
+ module.exports.wisp = esm.wisp;
24
+ module.exports.wispSync = esm.wispSync;
package/index.mjs ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @fileoverview Main entry point for @cldmv/wisp, providing version-agnostic JSON importing.
3
+ * @module @cldmv/wisp
4
+ * @public
5
+ *
6
+ * @description
7
+ * This module exports the wisp and wispSync functions for loading JSON files
8
+ * with support for different Node.js versions and import syntaxes.
9
+ *
10
+ * @example
11
+ * // ESM
12
+ * import { wisp, wispSync } from '@cldmv/wisp';
13
+ * const data = await wisp('./data.json');
14
+ *
15
+ * @example
16
+ * // CJS
17
+ * const { wisp, wispSync } = require('@cldmv/wisp');
18
+ * const data = wispSync('./data.json');
19
+ */
20
+
21
+ export * from "./src/wisp.mjs";
22
+ export { default } from "./src/wisp.mjs";
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@cldmv/wisp",
3
+ "version": "1.0.0",
4
+ "description": "Version-agnostic JSON importing for Node.js with fallback handling and caller path resolution.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./index.mjs",
9
+ "require": "./index.cjs"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "test": "mocha --recursive \"test/**/*.mjs\"",
14
+ "test:watch": "mocha --watch \"test/**/*.mjs\"",
15
+ "lint": "eslint src/",
16
+ "build:types": "tsc",
17
+ "test:types": "tsc --noEmit",
18
+ "build:ci": "npm run test && npm run build:types && npm run test:types"
19
+ },
20
+ "keywords": [
21
+ "json",
22
+ "import",
23
+ "nodejs",
24
+ "version-agnostic",
25
+ "fallback",
26
+ "loader",
27
+ "esm",
28
+ "cjs",
29
+ "wisp",
30
+ "api"
31
+ ],
32
+ "author": {
33
+ "name": "Shinrai",
34
+ "company": "CLDMV",
35
+ "email": "git+npm@cldmv.net",
36
+ "url": "https://cldmv.net"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/CLDMV/wisp.git"
41
+ },
42
+ "contributors": [
43
+ {
44
+ "name": "CLDMV",
45
+ "url": "https://github.com/CLDMV"
46
+ }
47
+ ],
48
+ "bugs": {
49
+ "url": "https://github.com/CLDMV/wisp/issues"
50
+ },
51
+ "homepage": "https://github.com/CLDMV/wisp#readme",
52
+ "license": "MIT",
53
+ "funding": {
54
+ "type": "github",
55
+ "url": "https://github.com/sponsors/shinrai"
56
+ },
57
+ "files": [
58
+ "src/",
59
+ "types/",
60
+ "index.mjs",
61
+ "index.cjs",
62
+ "README.md",
63
+ "LICENSE",
64
+ "package.json"
65
+ ],
66
+ "devDependencies": {
67
+ "mocha": "^10.2.0",
68
+ "chai": "^4.3.10",
69
+ "typescript": "^5.2.0",
70
+ "@types/node": "^20.8.0"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "engines": {
76
+ "node": ">=16.0.0"
77
+ },
78
+ "sideEffects": false
79
+ }
@@ -0,0 +1,440 @@
1
+ /**
2
+ * @Project: @cldmv/wisp
3
+ * @Filename: /src/lib/resolve-from-caller.mjs
4
+ * @Date: 2025-09-09 13:22:38 -07:00 (1757449358)
5
+ * @Author: Nate Hyson <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2025-10-31 07:31:40 -07:00 (1761921100)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ /**
15
+ * @fileoverview Path resolution utilities for resolving paths from the caller context. Internal file (not exported in package.json).
16
+ * @module @cldmv/wisp.helpers.resolve-from-caller
17
+ * @memberof module:@cldmv/wisp.helpers
18
+ * @internal
19
+ * @package
20
+ *
21
+ * @description
22
+ * Advanced path resolution system that uses V8 stack trace analysis to determine caller context.
23
+ * Provides utilities for resolving relative paths from the caller's directory, implementing
24
+ * sophisticated caller detection algorithms to handle complex module loading scenarios.
25
+ *
26
+ * Key features:
27
+ * - V8 CallSite-based stack trace analysis
28
+ * - Primary base file detection with fallback strategies
29
+ * - Support for both filesystem paths and file:// URLs
30
+ * - Smart handling of slothlet.mjs and index file patterns
31
+ * - Existence-based resolution with automatic fallback
32
+ *
33
+ * Technical implementation:
34
+ * - Uses Error.prepareStackTrace to access V8 CallSite objects
35
+ * - Implements dual-phase resolution (primary + fallback)
36
+ * - Handles edge cases like node:internal modules and helper directories
37
+ * - Provides both path and URL resolution variants
38
+ *
39
+ *
40
+ * @example
41
+ * // ESM (internal)
42
+ * import { resolvePathFromCaller } from "@cldmv/slothlet/helpers/resolve-from-caller";
43
+ * // Internal example using package.json exports
44
+ *
45
+ * @example
46
+ * // Relative import (internal)
47
+ * import { resolvePathFromCaller, resolveUrlFromCaller } from "./resolve-from-caller.mjs";
48
+ * const configPath = resolvePathFromCaller("../config.json");
49
+ */
50
+
51
+ import fs from "node:fs";
52
+ import path from "node:path";
53
+ import { fileURLToPath, pathToFileURL } from "node:url";
54
+
55
+ /* ---------- tiny utils ---------- */
56
+
57
+ /**
58
+ * @function toFsPath
59
+ * @package
60
+ * @internal
61
+ * @param {any} v - Value to convert (file:// URL, path string, or any other value)
62
+ * @returns {string|null} Filesystem path if URL conversion successful, original string if already a path, null if input is falsy
63
+ *
64
+ * @description
65
+ * Convert file:// URL to filesystem path, or return string as-is.
66
+ * Handles URL-to-path conversion for cross-platform compatibility while preserving
67
+ * non-URL strings unchanged. Provides null-safe operation for invalid inputs.
68
+ *
69
+ * @example
70
+ * // URL conversion
71
+ * toFsPath("file:///project/config.json"); // "/project/config.json" (Unix) or "C:\\project\\config.json" (Windows)
72
+ *
73
+ * @example
74
+ * // String passthrough
75
+ * toFsPath("/project/config.json"); // "/project/config.json"
76
+ * toFsPath("C:\\project\\config.json"); // "C:\\project\\config.json"
77
+ *
78
+ * @example
79
+ * // Null-safe handling
80
+ * toFsPath(null); // null
81
+ * toFsPath(undefined); // null
82
+ * toFsPath(""); // null
83
+ */
84
+ export const toFsPath = (v) => (v && String(v).startsWith("file://") ? fileURLToPath(String(v)) : v ? String(v) : null);
85
+
86
+ /**
87
+ * @function getStack
88
+ * @package
89
+ * @internal
90
+ * @param {Function} [skipFn] - Function to skip in stack trace via Error.captureStackTrace
91
+ * @returns {Array<CallSite>} Array of V8 CallSite objects with methods like getFileName(), getLineNumber(), etc.
92
+ *
93
+ * @description
94
+ * Get V8 stack trace as CallSite array for debugging and caller detection.
95
+ * Temporarily overrides Error.prepareStackTrace to access raw V8 CallSite objects
96
+ * instead of formatted string stack traces. Provides safe restoration of original handler.
97
+ *
98
+ * @example
99
+ * // Get current call stack for debugging
100
+ * const stack = getStack();
101
+ * console.log(stack[0]?.getFileName?.()); // Current file path
102
+ * console.log(stack[0]?.getLineNumber?.()); // Current line number
103
+ *
104
+ * @example
105
+ * // Skip current function from stack trace
106
+ * function myFunction() {
107
+ * return getStack(myFunction); // Stack starts from caller of myFunction
108
+ * }
109
+ * const callerStack = myFunction();
110
+ *
111
+ * @example
112
+ * // Stack analysis for caller detection
113
+ * function findCaller() {
114
+ * const stack = getStack(findCaller);
115
+ * for (const frame of stack) {
116
+ * const filename = frame?.getFileName?.();
117
+ * if (filename && !filename.includes("node_modules")) {
118
+ * return filename; // First non-dependency file
119
+ * }
120
+ * }
121
+ * }
122
+ */
123
+ export function getStack(skipFn) {
124
+ const orig = Error.prepareStackTrace;
125
+ try {
126
+ Error.prepareStackTrace = (_, s) => s; // V8 CallSite[]
127
+ const e = new Error("Stack trace");
128
+ if (skipFn) Error.captureStackTrace(e, skipFn);
129
+ return /** @type {NodeJS.CallSite[]} */ (/** @type {unknown} */ (e.stack)) || [];
130
+ } finally {
131
+ Error.prepareStackTrace = orig;
132
+ }
133
+ }
134
+
135
+ const THIS_FILE = fileURLToPath(import.meta.url);
136
+ const THIS_DIR = path.dirname(THIS_FILE);
137
+
138
+ /* ---------- base selection (shared) ---------- */
139
+ // Rule you specified:
140
+ // 1) Find the LAST frame whose basename is "slothlet.mjs".
141
+ // 2) Take the NEXT frame; if its basename is "index.[mjs|cjs|js]", take the following one.
142
+ // 3) Fallback: first non-helper frame that isn’t slothlet.mjs.
143
+
144
+ /**
145
+ * @function pickPrimaryBaseFile
146
+ * @internal
147
+ * @private
148
+ * @returns {string|null} Primary base file path for resolution, null if detection fails
149
+ *
150
+ * @description
151
+ * Find the primary base file using stack trace analysis.
152
+ * Implements sophisticated caller detection algorithm designed for slothlet's module loading patterns.
153
+ * Uses a two-phase approach: locate the last slothlet.mjs frame, then find the actual user code.
154
+ *
155
+ * Algorithm:
156
+ * 1. Scan stack trace to find the LAST frame whose basename is "slothlet.mjs"
157
+ * 2. Take the NEXT frame after slothlet.mjs
158
+ * 3. If that frame is "index.[mjs|cjs|js]", take the following frame instead
159
+ * 4. Return null if no suitable frame found (fallback will be used)
160
+ *
161
+ * This pattern handles slothlet's loading chain: slothlet.mjs → index.mjs → user-code.mjs
162
+ *
163
+ * @example
164
+ * // Stack trace scenario:
165
+ * // 0: pickPrimaryBaseFile() [this function]
166
+ * // 1: resolveWith() [internal helper]
167
+ * // 2: resolvePathFromCaller() [public API]
168
+ * // 3: slothlet.mjs [slothlet loader] ← LAST slothlet.mjs
169
+ * // 4: index.mjs [entry point] ← Skip this
170
+ * // 5: user-code.mjs [actual caller] ← Return this
171
+ */
172
+ function pickPrimaryBaseFile() {
173
+ const files = [];
174
+ for (const cs of getStack(pickPrimaryBaseFile)) {
175
+ const f = toFsPath(cs?.getFileName?.());
176
+ if (!f) continue;
177
+ if (f.startsWith?.("node:internal")) continue;
178
+ files.push(f);
179
+ }
180
+
181
+ let iSloth = -1;
182
+ for (let i = 0; i < files.length; i++) {
183
+ if (path.basename(files[i]).toLowerCase() === "slothlet.mjs") iSloth = i;
184
+ }
185
+ if (iSloth !== -1) {
186
+ const j = iSloth + 1;
187
+ if (j < files.length) {
188
+ const b = path.basename(files[j]).toLowerCase();
189
+ if (/^index\.(mjs|cjs|js)$/.test(b) && j + 1 < files.length) return files[j + 1];
190
+ return files[j];
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * @function pickFallbackBaseFile
198
+ * @internal
199
+ * @private
200
+ * @returns {string} Fallback base file path, guaranteed to return a valid path
201
+ *
202
+ * @description
203
+ * Find fallback base file when primary detection fails.
204
+ * Provides robust fallback strategy by finding the first legitimate user code frame.
205
+ * Filters out internal Node.js modules, helper utilities, and slothlet infrastructure.
206
+ *
207
+ * Fallback algorithm:
208
+ * 1. Iterate through stack frames from top to bottom
209
+ * 2. Skip node:internal modules (Node.js internals)
210
+ * 3. Skip this file itself (resolve-from-caller.mjs)
211
+ * 4. Skip other files in the helpers directory
212
+ * 5. Skip slothlet.mjs infrastructure files
213
+ * 6. Return first remaining frame, or THIS_FILE as ultimate fallback
214
+ *
215
+ * This ensures we always have a base path for resolution, even in edge cases.
216
+ *
217
+ * @example
218
+ * // Fallback scenarios:
219
+ * // - Primary detection failed (no slothlet.mjs in stack)
220
+ * // - Complex loading chain with multiple loaders
221
+ * // - Edge cases like REPL or test environments
222
+ * // - Direct API usage without slothlet loader
223
+ */
224
+ function pickFallbackBaseFile() {
225
+ for (const cs of getStack(pickFallbackBaseFile)) {
226
+ const f = toFsPath(cs?.getFileName?.());
227
+ if (!f) continue;
228
+ if (f.startsWith?.("node:internal")) continue;
229
+ if (f === THIS_FILE) continue;
230
+ if (f.startsWith(THIS_DIR + path.sep)) continue; // helper’s own dir
231
+ if (path.basename(f).toLowerCase() === "slothlet.mjs") continue;
232
+ return f;
233
+ }
234
+ return THIS_FILE;
235
+ }
236
+
237
+ /* ---------- generic resolver (shared) ---------- */
238
+
239
+ /**
240
+ * @function resolveWith
241
+ * @internal
242
+ * @private
243
+ * @param {string} rel - Relative path to resolve (must be a string)
244
+ * @param {Function} makePrimary - Function to build primary candidate: (baseFile, rel) => candidate
245
+ * @param {Function} exists - Existence check function: (candidate) => boolean
246
+ * @param {Function} makeFallback - Function to build fallback result: (baseFile, rel) => result
247
+ * @returns {string} Resolved path or URL (type depends on make functions)
248
+ * @throws {TypeError} When rel parameter is not a string
249
+ *
250
+ * @description
251
+ * Generic resolver that tries primary base file detection with fallback.
252
+ * Core resolution engine that orchestrates the two-phase detection strategy.
253
+ * Attempts primary resolution first, then falls back if target doesn't exist.
254
+ *
255
+ * Resolution strategy:
256
+ * 1. Get primary base file using sophisticated stack analysis
257
+ * 2. Build primary candidate using makePrimary function
258
+ * 3. Check if primary candidate exists using exists function
259
+ * 4. If exists, return primary candidate
260
+ * 5. Otherwise, get fallback base file and build fallback result
261
+ * 6. Return fallback result (no second existence check)
262
+ *
263
+ * This pattern ensures we try the most accurate resolution first, but always
264
+ * provide a reasonable fallback even if the target doesn't exist yet.
265
+ *
266
+ * @example
267
+ * // Usage pattern for filesystem paths:
268
+ * resolveWith(
269
+ * "../config.json",
270
+ * (base, rel) => path.resolve(path.dirname(base), rel), // makePrimary
271
+ * (candidate) => fs.existsSync(candidate), // exists
272
+ * (base, rel) => path.resolve(path.dirname(base), rel) // makeFallback
273
+ * );
274
+ *
275
+ * @example
276
+ * // Usage pattern for URLs:
277
+ * resolveWith(
278
+ * "../config.json",
279
+ * (base, rel) => new URL(rel, pathToFileURL(base)).href, // makePrimary
280
+ * (href) => fs.existsSync(fileURLToPath(href)), // exists
281
+ * (base, rel) => new URL(rel, pathToFileURL(base)).href // makeFallback
282
+ * );
283
+ */
284
+ function resolveWith(rel, makePrimary, exists, makeFallback) {
285
+ if (typeof rel !== "string") throw new TypeError("rel must be a string");
286
+
287
+ // absolute / already-URL cases are handled in the public wrappers
288
+ const primaryBase = pickPrimaryBaseFile() ?? pickFallbackBaseFile();
289
+ const primary = makePrimary(primaryBase, rel);
290
+ if (exists(primary)) return primary;
291
+
292
+ const fbBase = pickFallbackBaseFile();
293
+ return makeFallback(fbBase, rel);
294
+ }
295
+
296
+ /* ---------- public API (thin wrappers) ---------- */
297
+
298
+ /**
299
+ * @function resolvePathFromCaller
300
+ * @package
301
+ * @internal
302
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
303
+ * @returns {string} Absolute filesystem path with platform-specific separators
304
+ * @throws {TypeError} When rel parameter is not a string
305
+ *
306
+ * @description
307
+ * Resolve a relative path from the caller's context to an absolute filesystem path.
308
+ * Primary public API for filesystem path resolution with intelligent caller detection.
309
+ * Uses sophisticated stack trace analysis to determine the appropriate base directory.
310
+ *
311
+ * Resolution behavior:
312
+ * - file:// URLs: Converted to filesystem paths via fileURLToPath()
313
+ * - Absolute paths: Returned unchanged (already absolute)
314
+ * - Relative paths: Resolved using caller detection algorithm
315
+ *
316
+ * Caller detection process:
317
+ * 1. Primary: Use sophisticated slothlet.mjs-aware stack analysis
318
+ * 2. Fallback: Use first non-helper frame if primary fails
319
+ * 3. Existence check: Prefer primary if target exists, otherwise use fallback
320
+ *
321
+ * @example
322
+ * // From a file at /project/src/modules/math.mjs
323
+ * const configPath = resolvePathFromCaller("../config.json");
324
+ * // Returns: /project/config.json (absolute filesystem path)
325
+ *
326
+ * @example
327
+ * // Short-circuit cases
328
+ * resolvePathFromCaller("file:///absolute/path.txt");
329
+ * // Returns: /absolute/path.txt (converted from URL)
330
+ *
331
+ * resolvePathFromCaller("/already/absolute/path.txt");
332
+ * // Returns: /already/absolute/path.txt (unchanged)
333
+ *
334
+ * @example
335
+ * // Relative resolution from different contexts
336
+ * // If called from /project/src/lib/utils.mjs:
337
+ * resolvePathFromCaller("./helpers/format.js");
338
+ * // Returns: /project/src/lib/helpers/format.js
339
+ *
340
+ * resolvePathFromCaller("../../config/settings.json");
341
+ * // Returns: /project/config/settings.json
342
+ */
343
+ export function resolvePathFromCaller(rel) {
344
+ // short-circuits
345
+ if (rel.startsWith?.("file://")) return fileURLToPath(rel);
346
+ if (path.isAbsolute(rel)) return rel;
347
+
348
+ return resolveWith(
349
+ rel,
350
+ // makePrimary (PATH)
351
+ (baseFile, r) => path.resolve(path.dirname(baseFile), r),
352
+ // exists (PATH)
353
+ (candidate) => fs.existsSync(candidate),
354
+ // makeFallback (PATH)
355
+ (baseFile, r) => path.resolve(path.dirname(baseFile), r)
356
+ );
357
+ }
358
+
359
+ /**
360
+ * @function resolveUrlFromCaller
361
+ * @package
362
+ * @internal
363
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
364
+ * @returns {string} Absolute file:// URL suitable for dynamic imports and URL operations
365
+ * @throws {TypeError} When rel parameter is not a string
366
+ *
367
+ * @description
368
+ * Resolve a relative path from the caller's context to a file:// URL.
369
+ * Companion API to resolvePathFromCaller that returns file:// URLs instead of filesystem paths.
370
+ * Uses identical caller detection algorithm but outputs URL format for ESM imports.
371
+ *
372
+ * Resolution behavior:
373
+ * - file:// URLs: Returned unchanged (already in URL format)
374
+ * - Absolute paths: Converted to file:// URLs via pathToFileURL()
375
+ * - Relative paths: Resolved using caller detection, then converted to URL
376
+ *
377
+ * Caller detection uses the same sophisticated algorithm as resolvePathFromCaller,
378
+ * but the final result is converted to a file:// URL for compatibility with
379
+ * ESM dynamic imports and other URL-based operations.
380
+ *
381
+ * @example
382
+ * // From a file at /project/src/modules/math.mjs
383
+ * const configUrl = resolveUrlFromCaller("../config.json");
384
+ * // Returns: file:///project/config.json (absolute file:// URL)
385
+ *
386
+ * @example
387
+ * // Short-circuit cases
388
+ * resolveUrlFromCaller("file:///absolute/path.txt");
389
+ * // Returns: file:///absolute/path.txt (unchanged)
390
+ *
391
+ * resolveUrlFromCaller("/already/absolute/path.txt");
392
+ * // Returns: file:///already/absolute/path.txt (converted to URL)
393
+ *
394
+ * @example
395
+ * // Dynamic ESM import usage
396
+ * const modulePath = resolveUrlFromCaller("./dynamic-module.mjs");
397
+ * const dynamicModule = await import(modulePath);
398
+ * // Works seamlessly with ESM import() which expects URLs
399
+ *
400
+ * @example
401
+ * // Cross-platform URL handling
402
+ * // Unix: resolveUrlFromCaller("../config.json") → file:///project/config.json
403
+ * // Windows: resolveUrlFromCaller("../config.json") → file:///C:/project/config.json
404
+ */
405
+ export function resolveUrlFromCaller(rel) {
406
+ // short-circuits
407
+ if (rel.startsWith?.("file://")) return rel;
408
+ if (path.isAbsolute(rel)) return pathToFileURL(rel).href;
409
+
410
+ return resolveWith(
411
+ rel,
412
+ // makePrimary (URL)
413
+ (baseFile, r) => new URL(r, pathToFileURL(baseFile)).href,
414
+ // exists (URL→check target path)
415
+ (href) => fs.existsSync(fileURLToPath(href)),
416
+ // makeFallback (URL)
417
+ (baseFile, r) => new URL(r, pathToFileURL(baseFile)).href
418
+ );
419
+ }
420
+
421
+ /**
422
+ * @typedef {object} CallSite
423
+ * @property {function(): string|undefined} getFileName
424
+ * @property {function(): number|undefined} getLineNumber
425
+ * @property {function(): string|undefined} getFunctionName
426
+ * @property {function(): string|undefined} getTypeName
427
+ * @property {function(): string|undefined} getMethodName
428
+ * @property {function(): string|undefined} getScriptNameOrSourceURL
429
+ * @property {function(): number|undefined} getColumnNumber
430
+ * @property {function(): boolean|undefined} isNative
431
+ * @property {function(): boolean|undefined} isEval
432
+ * @property {function(): boolean|undefined} isConstructor
433
+ * @property {function(): boolean|undefined} isToplevel
434
+ * @property {function(): boolean|undefined} isAsync
435
+ * @property {function(): boolean|undefined} isPromiseAll
436
+ * @property {function(): number|undefined} getPromiseIndex
437
+ *
438
+ * @description
439
+ * Minimal V8 CallSite object type for stack trace analysis. Only includes methods used in this module.
440
+ */
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @function getStack
3
+ * @package
4
+ * @internal
5
+ * @param {Function} [skipFn] - Function to skip in stack trace via Error.captureStackTrace
6
+ * @returns {Array<CallSite>} Array of V8 CallSite objects with methods like getFileName(), getLineNumber(), etc.
7
+ *
8
+ * @description
9
+ * Get V8 stack trace as CallSite array for debugging and caller detection.
10
+ * Temporarily overrides Error.prepareStackTrace to access raw V8 CallSite objects
11
+ * instead of formatted string stack traces. Provides safe restoration of original handler.
12
+ *
13
+ * @example
14
+ * // Get current call stack for debugging
15
+ * const stack = getStack();
16
+ * console.log(stack[0]?.getFileName?.()); // Current file path
17
+ * console.log(stack[0]?.getLineNumber?.()); // Current line number
18
+ *
19
+ * @example
20
+ * // Skip current function from stack trace
21
+ * function myFunction() {
22
+ * return getStack(myFunction); // Stack starts from caller of myFunction
23
+ * }
24
+ * const callerStack = myFunction();
25
+ *
26
+ * @example
27
+ * // Stack analysis for caller detection
28
+ * function findCaller() {
29
+ * const stack = getStack(findCaller);
30
+ * for (const frame of stack) {
31
+ * const filename = frame?.getFileName?.();
32
+ * if (filename && !filename.includes("node_modules")) {
33
+ * return filename; // First non-dependency file
34
+ * }
35
+ * }
36
+ * }
37
+ */
38
+ export function getStack(skipFn?: Function): Array<CallSite>;
39
+ /**
40
+ * @function resolvePathFromCaller
41
+ * @package
42
+ * @internal
43
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
44
+ * @returns {string} Absolute filesystem path with platform-specific separators
45
+ * @throws {TypeError} When rel parameter is not a string
46
+ *
47
+ * @description
48
+ * Resolve a relative path from the caller's context to an absolute filesystem path.
49
+ * Primary public API for filesystem path resolution with intelligent caller detection.
50
+ * Uses sophisticated stack trace analysis to determine the appropriate base directory.
51
+ *
52
+ * Resolution behavior:
53
+ * - file:// URLs: Converted to filesystem paths via fileURLToPath()
54
+ * - Absolute paths: Returned unchanged (already absolute)
55
+ * - Relative paths: Resolved using caller detection algorithm
56
+ *
57
+ * Caller detection process:
58
+ * 1. Primary: Use sophisticated slothlet.mjs-aware stack analysis
59
+ * 2. Fallback: Use first non-helper frame if primary fails
60
+ * 3. Existence check: Prefer primary if target exists, otherwise use fallback
61
+ *
62
+ * @example
63
+ * // From a file at /project/src/modules/math.mjs
64
+ * const configPath = resolvePathFromCaller("../config.json");
65
+ * // Returns: /project/config.json (absolute filesystem path)
66
+ *
67
+ * @example
68
+ * // Short-circuit cases
69
+ * resolvePathFromCaller("file:///absolute/path.txt");
70
+ * // Returns: /absolute/path.txt (converted from URL)
71
+ *
72
+ * resolvePathFromCaller("/already/absolute/path.txt");
73
+ * // Returns: /already/absolute/path.txt (unchanged)
74
+ *
75
+ * @example
76
+ * // Relative resolution from different contexts
77
+ * // If called from /project/src/lib/utils.mjs:
78
+ * resolvePathFromCaller("./helpers/format.js");
79
+ * // Returns: /project/src/lib/helpers/format.js
80
+ *
81
+ * resolvePathFromCaller("../../config/settings.json");
82
+ * // Returns: /project/config/settings.json
83
+ */
84
+ export function resolvePathFromCaller(rel: string): string;
85
+ /**
86
+ * @function resolveUrlFromCaller
87
+ * @package
88
+ * @internal
89
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
90
+ * @returns {string} Absolute file:// URL suitable for dynamic imports and URL operations
91
+ * @throws {TypeError} When rel parameter is not a string
92
+ *
93
+ * @description
94
+ * Resolve a relative path from the caller's context to a file:// URL.
95
+ * Companion API to resolvePathFromCaller that returns file:// URLs instead of filesystem paths.
96
+ * Uses identical caller detection algorithm but outputs URL format for ESM imports.
97
+ *
98
+ * Resolution behavior:
99
+ * - file:// URLs: Returned unchanged (already in URL format)
100
+ * - Absolute paths: Converted to file:// URLs via pathToFileURL()
101
+ * - Relative paths: Resolved using caller detection, then converted to URL
102
+ *
103
+ * Caller detection uses the same sophisticated algorithm as resolvePathFromCaller,
104
+ * but the final result is converted to a file:// URL for compatibility with
105
+ * ESM dynamic imports and other URL-based operations.
106
+ *
107
+ * @example
108
+ * // From a file at /project/src/modules/math.mjs
109
+ * const configUrl = resolveUrlFromCaller("../config.json");
110
+ * // Returns: file:///project/config.json (absolute file:// URL)
111
+ *
112
+ * @example
113
+ * // Short-circuit cases
114
+ * resolveUrlFromCaller("file:///absolute/path.txt");
115
+ * // Returns: file:///absolute/path.txt (unchanged)
116
+ *
117
+ * resolveUrlFromCaller("/already/absolute/path.txt");
118
+ * // Returns: file:///already/absolute/path.txt (converted to URL)
119
+ *
120
+ * @example
121
+ * // Dynamic ESM import usage
122
+ * const modulePath = resolveUrlFromCaller("./dynamic-module.mjs");
123
+ * const dynamicModule = await import(modulePath);
124
+ * // Works seamlessly with ESM import() which expects URLs
125
+ *
126
+ * @example
127
+ * // Cross-platform URL handling
128
+ * // Unix: resolveUrlFromCaller("../config.json") → file:///project/config.json
129
+ * // Windows: resolveUrlFromCaller("../config.json") → file:///C:/project/config.json
130
+ */
131
+ export function resolveUrlFromCaller(rel: string): string;
132
+ export function toFsPath(v: any): string | null;
133
+ export type CallSite = {
134
+ getFileName: () => string | undefined;
135
+ getLineNumber: () => number | undefined;
136
+ getFunctionName: () => string | undefined;
137
+ getTypeName: () => string | undefined;
138
+ getMethodName: () => string | undefined;
139
+ getScriptNameOrSourceURL: () => string | undefined;
140
+ getColumnNumber: () => number | undefined;
141
+ isNative: () => boolean | undefined;
142
+ isEval: () => boolean | undefined;
143
+ isConstructor: () => boolean | undefined;
144
+ isToplevel: () => boolean | undefined;
145
+ isAsync: () => boolean | undefined;
146
+ isPromiseAll: () => boolean | undefined;
147
+ getPromiseIndex: () => number | undefined;
148
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Asynchronously loads JSON from a file, trying modern import with 'with', then 'assert', then fs.
3
+ * @public
4
+ * @param {string|URL} input - The path or URL to the JSON file.
5
+ * @param {Object} [options] - Options object.
6
+ * @param {string|URL} [options.base] - Base URL for relative paths.
7
+ * @param {Function} [options.validate] - Validation function for the parsed JSON.
8
+ * @param {Function} [options.reviver] - Reviver function for JSON.parse.
9
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
10
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
11
+ * @returns {Promise<*>} The parsed JSON value.
12
+ *
13
+ * @description
14
+ * Loads JSON asynchronously with version-agnostic support.
15
+ * Attempts modern import syntax first, falls back to legacy assert, then fs.readFile.
16
+ *
17
+ * @example
18
+ * import { wisp } from '@cldmv/wisp';
19
+ * const data = await wisp('./config.json');
20
+ */
21
+ export function wisp(input: string | URL, options?: {
22
+ base?: string | URL;
23
+ validate?: Function;
24
+ reviver?: Function;
25
+ type?: string;
26
+ fallback?: string | URL;
27
+ }): Promise<any>;
28
+ /**
29
+ * Synchronously loads JSON from a file using fs.
30
+ * @public
31
+ * @param {string|URL} input - The path or URL to the JSON file.
32
+ * @param {Object} [options] - Options object.
33
+ * @param {string|URL} [options.base] - Base URL for relative paths.
34
+ * @param {Function} [options.validate] - Validation function for the parsed JSON.
35
+ * @param {Function} [options.reviver] - Reviver function for JSON.parse.
36
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
37
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
38
+ * @returns {*} The parsed JSON value.
39
+ *
40
+ * @description
41
+ * Loads JSON synchronously using fs.readFileSync.
42
+ *
43
+ * @example
44
+ * import { wispSync } from '@cldmv/wisp';
45
+ * const data = wispSync('./config.json');
46
+ */
47
+ export function wispSync(input: string | URL, options?: {
48
+ base?: string | URL;
49
+ validate?: Function;
50
+ reviver?: Function;
51
+ type?: string;
52
+ fallback?: string | URL;
53
+ }): any;
54
+ export default wisp;
package/src/wisp.mjs ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @Project: @cldmv/wisp
3
+ * @Filename: /src/wisp.mjs
4
+ * @Date: 2025-10-30 14:12:05 -07:00 (1761858725)
5
+ * @Author: Nate Hyson <CLDMV>
6
+ * @Email: <Shinrai@users.noreply.github.com>
7
+ * -----
8
+ * @Last modified by: Nate Hyson <CLDMV> (Shinrai@users.noreply.github.com)
9
+ * @Last modified time: 2025-10-30 14:59:15 -07:00 (1761861555)
10
+ * -----
11
+ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved.
12
+ */
13
+
14
+ /**
15
+ * @fileoverview Internal implementation of @cldmv/wisp. Not exported in package.json.
16
+ * @module @cldmv/wisp.src.wisp
17
+ * @internal
18
+ * @private
19
+ *
20
+ * @description
21
+ * This module provides the core implementation for version-agnostic JSON importing in Node.js.
22
+ * It includes functions to load JSON asynchronously and synchronously, with fallbacks for different Node versions.
23
+ *
24
+ * @example
25
+ * // Internal usage example
26
+ * import { wisp } from './src/wisp.mjs';
27
+ * const data = await wisp('./data.json');
28
+ */
29
+
30
+ import fs from "node:fs";
31
+ import { readFile } from "node:fs/promises";
32
+ import path from "node:path";
33
+ import { pathToFileURL } from "node:url";
34
+ import { resolveUrlFromCaller } from "./lib/resolve-from-caller.mjs";
35
+
36
+ /**
37
+ * Deep clones a value using structuredClone if available, otherwise JSON.parse/stringify.
38
+ * @private
39
+ * @param {*} v - The value to clone.
40
+ * @returns {*} The cloned value.
41
+ */
42
+ /**
43
+ * Deep clones a value using structuredClone if available, otherwise JSON.parse/stringify.
44
+ * @private
45
+ * @param {*} v - The value to clone.
46
+ * @returns {*} The cloned value.
47
+ */
48
+ function deepClone(v) {
49
+ return typeof globalThis.structuredClone === "function" ? globalThis.structuredClone(v) : JSON.parse(JSON.stringify(v));
50
+ }
51
+
52
+ /**
53
+ * Asynchronously loads JSON from a file, trying modern import with 'with', then 'assert', then fs.
54
+ * @public
55
+ * @param {string|URL} input - The path or URL to the JSON file.
56
+ * @param {Object} [options] - Options object.
57
+ * @param {string|URL} [options.base] - Base URL for relative paths.
58
+ * @param {(val: any) => void} [options.validate] - Validation function for the parsed JSON.
59
+ * @param {(this: any, key: string, value: any) => any} [options.reviver] - Reviver function for JSON.parse.
60
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
61
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
62
+ * @returns {Promise<*>} The parsed JSON value.
63
+ *
64
+ * @description
65
+ * Loads JSON asynchronously with version-agnostic support.
66
+ * Attempts modern import syntax first, falls back to legacy assert, then fs.readFile.
67
+ *
68
+ * @example
69
+ * import { wisp } from '@cldmv/wisp';
70
+ * const data = await wisp('./config.json');
71
+ */
72
+ export async function wisp(input, options = {}) {
73
+ const { base, validate, reviver, type = "json", fallback } = options;
74
+ let url;
75
+ if (input instanceof URL) url = input;
76
+ else {
77
+ const s = String(input);
78
+ if (s.startsWith("file://")) url = new URL(s);
79
+ else if (path.isAbsolute(s)) url = pathToFileURL(s);
80
+ else if (base) url = new URL(s, new URL(base));
81
+ else url = new URL(resolveUrlFromCaller(s));
82
+ }
83
+
84
+ try {
85
+ const mod = await import(url.href, { with: { type } });
86
+ if (!reviver && !validate) return mod?.default ?? mod;
87
+ let val = deepClone(mod?.default ?? mod);
88
+ if (reviver) val = JSON.parse(JSON.stringify(val), reviver);
89
+ if (validate) {
90
+ try {
91
+ validate(val);
92
+ } catch (e) {
93
+ throw new Error(`@cldmv/wisp: ${e?.message ?? e}`);
94
+ }
95
+ }
96
+ return val;
97
+ } catch {}
98
+
99
+ try {
100
+ const mod = await import(url.href, { assert: { type } });
101
+ if (!reviver && !validate) return mod?.default ?? mod;
102
+ let val = deepClone(mod?.default ?? mod);
103
+ if (reviver) val = JSON.parse(JSON.stringify(val), reviver);
104
+ if (validate) {
105
+ try {
106
+ validate(val);
107
+ } catch (e) {
108
+ throw new Error(`@cldmv/wisp: ${e?.message ?? e}`);
109
+ }
110
+ }
111
+ return val;
112
+ } catch {}
113
+
114
+ if (type === "json") {
115
+ try {
116
+ const txt = await readFile(url, "utf8");
117
+ const val = deepClone(JSON.parse(txt, reviver));
118
+ if (validate) {
119
+ try {
120
+ validate(val);
121
+ } catch (e) {
122
+ throw new Error(`@cldmv/wisp: ${e?.message ?? e}`);
123
+ }
124
+ }
125
+ return val;
126
+ } catch (e) {
127
+ if (fallback) {
128
+ return wisp(fallback, options);
129
+ }
130
+ throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`);
131
+ }
132
+ }
133
+
134
+ throw new Error(`@cldmv/wisp: Unsupported type '${type}' or failed to load module at ${url.href}`);
135
+ }
136
+
137
+ /**
138
+ * Synchronously loads JSON from a file using fs.
139
+ * @public
140
+ * @param {string|URL} input - The path or URL to the JSON file.
141
+ * @param {Object} [options] - Options object.
142
+ * @param {string|URL} [options.base] - Base URL for relative paths.
143
+ * @param {(val: any) => void} [options.validate] - Validation function for the parsed JSON.
144
+ * @param {(this: any, key: string, value: any) => any} [options.reviver] - Reviver function for JSON.parse.
145
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
146
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
147
+ * @returns {*} The parsed JSON value.
148
+ *
149
+ * @description
150
+ * Loads JSON synchronously using fs.readFileSync.
151
+ *
152
+ * @example
153
+ * import { wispSync } from '@cldmv/wisp';
154
+ * const data = wispSync('./config.json');
155
+ */
156
+ export function wispSync(input, options = {}) {
157
+ const { base, validate, reviver, type = "json", fallback } = options;
158
+ let url;
159
+ if (input instanceof URL) url = input;
160
+ else {
161
+ const s = String(input);
162
+ if (s.startsWith("file://")) url = new URL(s);
163
+ else if (path.isAbsolute(s)) url = pathToFileURL(s);
164
+ else if (base) url = new URL(s, new URL(base));
165
+ else url = new URL(resolveUrlFromCaller(s));
166
+ }
167
+
168
+ try {
169
+ const txt = fs.readFileSync(url, "utf8");
170
+ const val = deepClone(JSON.parse(txt, reviver));
171
+ if (validate) {
172
+ try {
173
+ validate(val);
174
+ } catch (e) {
175
+ throw new Error(`@cldmv/wisp: ${e?.message ?? e}`);
176
+ }
177
+ }
178
+ return val;
179
+ } catch (e) {
180
+ if (fallback) {
181
+ return wispSync(fallback, options);
182
+ }
183
+ throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`);
184
+ }
185
+ }
186
+
187
+ export default wisp;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @function getStack
3
+ * @package
4
+ * @internal
5
+ * @param {Function} [skipFn] - Function to skip in stack trace via Error.captureStackTrace
6
+ * @returns {Array<CallSite>} Array of V8 CallSite objects with methods like getFileName(), getLineNumber(), etc.
7
+ *
8
+ * @description
9
+ * Get V8 stack trace as CallSite array for debugging and caller detection.
10
+ * Temporarily overrides Error.prepareStackTrace to access raw V8 CallSite objects
11
+ * instead of formatted string stack traces. Provides safe restoration of original handler.
12
+ *
13
+ * @example
14
+ * // Get current call stack for debugging
15
+ * const stack = getStack();
16
+ * console.log(stack[0]?.getFileName?.()); // Current file path
17
+ * console.log(stack[0]?.getLineNumber?.()); // Current line number
18
+ *
19
+ * @example
20
+ * // Skip current function from stack trace
21
+ * function myFunction() {
22
+ * return getStack(myFunction); // Stack starts from caller of myFunction
23
+ * }
24
+ * const callerStack = myFunction();
25
+ *
26
+ * @example
27
+ * // Stack analysis for caller detection
28
+ * function findCaller() {
29
+ * const stack = getStack(findCaller);
30
+ * for (const frame of stack) {
31
+ * const filename = frame?.getFileName?.();
32
+ * if (filename && !filename.includes("node_modules")) {
33
+ * return filename; // First non-dependency file
34
+ * }
35
+ * }
36
+ * }
37
+ */
38
+ export function getStack(skipFn?: Function): Array<CallSite>;
39
+ /**
40
+ * @function resolvePathFromCaller
41
+ * @package
42
+ * @internal
43
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
44
+ * @returns {string} Absolute filesystem path with platform-specific separators
45
+ * @throws {TypeError} When rel parameter is not a string
46
+ *
47
+ * @description
48
+ * Resolve a relative path from the caller's context to an absolute filesystem path.
49
+ * Primary public API for filesystem path resolution with intelligent caller detection.
50
+ * Uses sophisticated stack trace analysis to determine the appropriate base directory.
51
+ *
52
+ * Resolution behavior:
53
+ * - file:// URLs: Converted to filesystem paths via fileURLToPath()
54
+ * - Absolute paths: Returned unchanged (already absolute)
55
+ * - Relative paths: Resolved using caller detection algorithm
56
+ *
57
+ * Caller detection process:
58
+ * 1. Primary: Use sophisticated slothlet.mjs-aware stack analysis
59
+ * 2. Fallback: Use first non-helper frame if primary fails
60
+ * 3. Existence check: Prefer primary if target exists, otherwise use fallback
61
+ *
62
+ * @example
63
+ * // From a file at /project/src/modules/math.mjs
64
+ * const configPath = resolvePathFromCaller("../config.json");
65
+ * // Returns: /project/config.json (absolute filesystem path)
66
+ *
67
+ * @example
68
+ * // Short-circuit cases
69
+ * resolvePathFromCaller("file:///absolute/path.txt");
70
+ * // Returns: /absolute/path.txt (converted from URL)
71
+ *
72
+ * resolvePathFromCaller("/already/absolute/path.txt");
73
+ * // Returns: /already/absolute/path.txt (unchanged)
74
+ *
75
+ * @example
76
+ * // Relative resolution from different contexts
77
+ * // If called from /project/src/lib/utils.mjs:
78
+ * resolvePathFromCaller("./helpers/format.js");
79
+ * // Returns: /project/src/lib/helpers/format.js
80
+ *
81
+ * resolvePathFromCaller("../../config/settings.json");
82
+ * // Returns: /project/config/settings.json
83
+ */
84
+ export function resolvePathFromCaller(rel: string): string;
85
+ /**
86
+ * @function resolveUrlFromCaller
87
+ * @package
88
+ * @internal
89
+ * @param {string} rel - Relative path to resolve (e.g., "../config.json", "./data/file.txt")
90
+ * @returns {string} Absolute file:// URL suitable for dynamic imports and URL operations
91
+ * @throws {TypeError} When rel parameter is not a string
92
+ *
93
+ * @description
94
+ * Resolve a relative path from the caller's context to a file:// URL.
95
+ * Companion API to resolvePathFromCaller that returns file:// URLs instead of filesystem paths.
96
+ * Uses identical caller detection algorithm but outputs URL format for ESM imports.
97
+ *
98
+ * Resolution behavior:
99
+ * - file:// URLs: Returned unchanged (already in URL format)
100
+ * - Absolute paths: Converted to file:// URLs via pathToFileURL()
101
+ * - Relative paths: Resolved using caller detection, then converted to URL
102
+ *
103
+ * Caller detection uses the same sophisticated algorithm as resolvePathFromCaller,
104
+ * but the final result is converted to a file:// URL for compatibility with
105
+ * ESM dynamic imports and other URL-based operations.
106
+ *
107
+ * @example
108
+ * // From a file at /project/src/modules/math.mjs
109
+ * const configUrl = resolveUrlFromCaller("../config.json");
110
+ * // Returns: file:///project/config.json (absolute file:// URL)
111
+ *
112
+ * @example
113
+ * // Short-circuit cases
114
+ * resolveUrlFromCaller("file:///absolute/path.txt");
115
+ * // Returns: file:///absolute/path.txt (unchanged)
116
+ *
117
+ * resolveUrlFromCaller("/already/absolute/path.txt");
118
+ * // Returns: file:///already/absolute/path.txt (converted to URL)
119
+ *
120
+ * @example
121
+ * // Dynamic ESM import usage
122
+ * const modulePath = resolveUrlFromCaller("./dynamic-module.mjs");
123
+ * const dynamicModule = await import(modulePath);
124
+ * // Works seamlessly with ESM import() which expects URLs
125
+ *
126
+ * @example
127
+ * // Cross-platform URL handling
128
+ * // Unix: resolveUrlFromCaller("../config.json") → file:///project/config.json
129
+ * // Windows: resolveUrlFromCaller("../config.json") → file:///C:/project/config.json
130
+ */
131
+ export function resolveUrlFromCaller(rel: string): string;
132
+ export function toFsPath(v: any): string | null;
133
+ export type CallSite = {
134
+ getFileName: () => string | undefined;
135
+ getLineNumber: () => number | undefined;
136
+ getFunctionName: () => string | undefined;
137
+ getTypeName: () => string | undefined;
138
+ getMethodName: () => string | undefined;
139
+ getScriptNameOrSourceURL: () => string | undefined;
140
+ getColumnNumber: () => number | undefined;
141
+ isNative: () => boolean | undefined;
142
+ isEval: () => boolean | undefined;
143
+ isConstructor: () => boolean | undefined;
144
+ isToplevel: () => boolean | undefined;
145
+ isAsync: () => boolean | undefined;
146
+ isPromiseAll: () => boolean | undefined;
147
+ getPromiseIndex: () => number | undefined;
148
+ };
149
+ //# sourceMappingURL=resolve-from-caller.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-from-caller.d.mts","sourceRoot":"","sources":["../../src/lib/resolve-from-caller.mjs"],"names":[],"mappings":"AAqFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,6CAhCa,KAAK,CAAC,QAAQ,CAAC,CA0C3B;AAqKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,2CAzCW,MAAM,GACJ,MAAM,CAsDlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0CA1CW,MAAM,GACJ,MAAM,CAuDlB;AA/UM,4BAvBI,GAAG,GACD,MAAM,GAAC,IAAI,CAsB+F;;iBAmVzG,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;iBAC5B,MAAY,MAAM,GAAC,SAAS;mBAC5B,MAAY,MAAM,GAAC,SAAS;8BAC5B,MAAY,MAAM,GAAC,SAAS;qBAC5B,MAAY,MAAM,GAAC,SAAS;cAC5B,MAAY,OAAO,GAAC,SAAS;YAC7B,MAAY,OAAO,GAAC,SAAS;mBAC7B,MAAY,OAAO,GAAC,SAAS;gBAC7B,MAAY,OAAO,GAAC,SAAS;aAC7B,MAAY,OAAO,GAAC,SAAS;kBAC7B,MAAY,OAAO,GAAC,SAAS;qBAC7B,MAAY,MAAM,GAAC,SAAS"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Asynchronously loads JSON from a file, trying modern import with 'with', then 'assert', then fs.
3
+ * @public
4
+ * @param {string|URL} input - The path or URL to the JSON file.
5
+ * @param {Object} [options] - Options object.
6
+ * @param {string|URL} [options.base] - Base URL for relative paths.
7
+ * @param {(val: any) => void} [options.validate] - Validation function for the parsed JSON.
8
+ * @param {(this: any, key: string, value: any) => any} [options.reviver] - Reviver function for JSON.parse.
9
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
10
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
11
+ * @returns {Promise<*>} The parsed JSON value.
12
+ *
13
+ * @description
14
+ * Loads JSON asynchronously with version-agnostic support.
15
+ * Attempts modern import syntax first, falls back to legacy assert, then fs.readFile.
16
+ *
17
+ * @example
18
+ * import { wisp } from '@cldmv/wisp';
19
+ * const data = await wisp('./config.json');
20
+ */
21
+ export function wisp(input: string | URL, options?: {
22
+ base?: string | URL;
23
+ validate?: (val: any) => void;
24
+ reviver?: (this: any, key: string, value: any) => any;
25
+ type?: string;
26
+ fallback?: string | URL;
27
+ }): Promise<any>;
28
+ /**
29
+ * Synchronously loads JSON from a file using fs.
30
+ * @public
31
+ * @param {string|URL} input - The path or URL to the JSON file.
32
+ * @param {Object} [options] - Options object.
33
+ * @param {string|URL} [options.base] - Base URL for relative paths.
34
+ * @param {(val: any) => void} [options.validate] - Validation function for the parsed JSON.
35
+ * @param {(this: any, key: string, value: any) => any} [options.reviver] - Reviver function for JSON.parse.
36
+ * @param {string} [options.type='json'] - The type of the file being imported. Defaults to 'json'.
37
+ * @param {string|URL} [options.fallback] - Fallback path or URL if the primary file is missing.
38
+ * @returns {*} The parsed JSON value.
39
+ *
40
+ * @description
41
+ * Loads JSON synchronously using fs.readFileSync.
42
+ *
43
+ * @example
44
+ * import { wispSync } from '@cldmv/wisp';
45
+ * const data = wispSync('./config.json');
46
+ */
47
+ export function wispSync(input: string | URL, options?: {
48
+ base?: string | URL;
49
+ validate?: (val: any) => void;
50
+ reviver?: (this: any, key: string, value: any) => any;
51
+ type?: string;
52
+ fallback?: string | URL;
53
+ }): any;
54
+ export default wisp;
55
+ //# sourceMappingURL=wisp.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wisp.d.mts","sourceRoot":"","sources":["../src/wisp.mjs"],"names":[],"mappings":"AAmDA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,4BAjBW,MAAM,GAAC,GAAG,YAElB;IAA6B,IAAI,GAAzB,MAAM,GAAC,GAAG;IACmB,QAAQ,GAArC,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI;IACoC,OAAO,GAA7D,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG;IAC1B,IAAI,GAArB,MAAM;IACe,QAAQ,GAA7B,MAAM,GAAC,GAAG;CAClB,GAAU,OAAO,CAAC,GAAC,CAAC,CAyEtB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,gCAhBW,MAAM,GAAC,GAAG,YAElB;IAA6B,IAAI,GAAzB,MAAM,GAAC,GAAG;IACmB,QAAQ,GAArC,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI;IACoC,OAAO,GAA7D,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG;IAC1B,IAAI,GAArB,MAAM;IACe,QAAQ,GAA7B,MAAM,GAAC,GAAG;CAClB,GAAU,GAAC,CAsCb"}