@cldmv/wisp 1.0.0 → 1.0.5
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cldmv/wisp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Version-agnostic JSON importing for Node.js with fallback handling and caller path resolution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "mocha --recursive \"test/**/*.mjs\"",
|
|
14
14
|
"test:watch": "mocha --watch \"test/**/*.mjs\"",
|
|
15
|
-
"lint": "eslint
|
|
15
|
+
"lint": "eslint --config .configs/eslint.config.mjs .",
|
|
16
16
|
"build:types": "tsc",
|
|
17
17
|
"test:types": "tsc --noEmit",
|
|
18
18
|
"build:ci": "npm run test && npm run build:types && npm run test:types"
|
|
@@ -64,10 +64,13 @@
|
|
|
64
64
|
"package.json"
|
|
65
65
|
],
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
67
|
+
"@eslint/js": "^10.0.1",
|
|
68
|
+
"@types/node": "^26.6.2",
|
|
69
|
+
"chai": "^6.2.2",
|
|
70
|
+
"eslint": "^10.10.0",
|
|
71
|
+
"globals": "^17.12.0",
|
|
72
|
+
"mocha": "^12.0.1",
|
|
73
|
+
"typescript": "^6.0.3"
|
|
71
74
|
},
|
|
72
75
|
"publishConfig": {
|
|
73
76
|
"access": "public"
|
|
@@ -133,13 +133,27 @@ export function getStack(skipFn) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
136
|
-
|
|
136
|
+
|
|
137
|
+
// Find the package root by looking for package.json
|
|
138
|
+
function findPackageRoot(startPath) {
|
|
139
|
+
let currentPath = startPath;
|
|
140
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
141
|
+
const packageJsonPath = path.join(currentPath, "package.json");
|
|
142
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
143
|
+
return currentPath;
|
|
144
|
+
}
|
|
145
|
+
currentPath = path.dirname(currentPath);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const PACKAGE_ROOT = findPackageRoot(THIS_FILE);
|
|
137
151
|
|
|
138
152
|
/* ---------- base selection (shared) ---------- */
|
|
139
|
-
//
|
|
140
|
-
// 1)
|
|
141
|
-
// 2)
|
|
142
|
-
// 3)
|
|
153
|
+
// Two-flag state machine for detecting when we've exited package infrastructure:
|
|
154
|
+
// 1) Flag 1: Set when we exit src/ or dist/ directories
|
|
155
|
+
// 2) Flag 2: Set when we exit index.* entry point files
|
|
156
|
+
// 3) Once both flags are set, we've found the actual caller outside the package
|
|
143
157
|
|
|
144
158
|
/**
|
|
145
159
|
* @function pickPrimaryBaseFile
|
|
@@ -148,51 +162,77 @@ const THIS_DIR = path.dirname(THIS_FILE);
|
|
|
148
162
|
* @returns {string|null} Primary base file path for resolution, null if detection fails
|
|
149
163
|
*
|
|
150
164
|
* @description
|
|
151
|
-
* Find the primary base file using stack trace analysis.
|
|
152
|
-
*
|
|
153
|
-
* Uses a two-phase approach: locate the last slothlet.mjs frame, then find the actual user code.
|
|
165
|
+
* Find the primary base file using stack trace analysis with a two-flag state machine.
|
|
166
|
+
* Tracks when we **exit** from package infrastructure directories/files.
|
|
154
167
|
*
|
|
155
168
|
* Algorithm:
|
|
156
|
-
* 1.
|
|
157
|
-
* 2.
|
|
158
|
-
* 3.
|
|
159
|
-
* 4.
|
|
160
|
-
*
|
|
161
|
-
* This pattern handles slothlet's loading chain: slothlet.mjs → index.mjs → user-code.mjs
|
|
169
|
+
* 1. Walk stack trace backwards (from deepest to shallowest calls)
|
|
170
|
+
* 2. Flag 1: Set when we exit src/ or dist/ folder (transition from src/dist to non-src/dist)
|
|
171
|
+
* 3. Flag 2: Set when we exit index.* files (transition from index.* to non-index.*)
|
|
172
|
+
* 4. Once both flags are set, we've fully exited the package infrastructure
|
|
173
|
+
* 5. Return the current file as the actual caller
|
|
162
174
|
*
|
|
163
175
|
* @example
|
|
164
|
-
* // Stack trace
|
|
165
|
-
* // 0:
|
|
166
|
-
* // 1:
|
|
167
|
-
* // 2:
|
|
168
|
-
* // 3:
|
|
169
|
-
* // 4: index.mjs [entry point] ← Skip this
|
|
170
|
-
* // 5: user-code.mjs [actual caller] ← Return this
|
|
176
|
+
* // Stack trace flow (wisp example):
|
|
177
|
+
* // 0: src/lib/resolve-from-caller.mjs [in src/]
|
|
178
|
+
* // 1: src/wisp.mjs [still in src/]
|
|
179
|
+
* // 2: index.mjs [exited src/ - Flag 1 set]
|
|
180
|
+
* // 3: user-code.mjs [exited index.* - Flag 2 set, return this!]
|
|
171
181
|
*/
|
|
172
182
|
function pickPrimaryBaseFile() {
|
|
173
|
-
|
|
183
|
+
let exitedSrcOrDist = false; // Flag 1: Have we exited src/ or dist/?
|
|
184
|
+
// This function's own docstring documents a "Flag 2 set, return this!"
|
|
185
|
+
// scenario, but nothing reads exitedIndex to return early once it's set --
|
|
186
|
+
// looks like a pre-existing gap between the documented intent and the
|
|
187
|
+
// implementation. Left as-is; fixing the behavior is a separate decision
|
|
188
|
+
// from unblocking lint.
|
|
189
|
+
let exitedIndex = false; // Flag 2: Have we exited index.*?
|
|
190
|
+
let previousWasInSrcOrDist = false;
|
|
191
|
+
let previousWasIndex = false;
|
|
192
|
+
|
|
174
193
|
for (const cs of getStack(pickPrimaryBaseFile)) {
|
|
175
194
|
const f = toFsPath(cs?.getFileName?.());
|
|
176
195
|
if (!f) continue;
|
|
177
196
|
if (f.startsWith?.("node:internal")) continue;
|
|
178
|
-
|
|
179
|
-
}
|
|
197
|
+
if (f === THIS_FILE) continue;
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
let currentIsInSrcOrDist = false;
|
|
200
|
+
let currentIsIndex = false;
|
|
201
|
+
|
|
202
|
+
// Check if this file is in our package
|
|
203
|
+
if (PACKAGE_ROOT && f.startsWith(PACKAGE_ROOT + path.sep)) {
|
|
204
|
+
const relativePath = path.relative(PACKAGE_ROOT, f);
|
|
205
|
+
const segments = relativePath.split(path.sep);
|
|
206
|
+
|
|
207
|
+
// Check if current file is in src/ or dist/
|
|
208
|
+
currentIsInSrcOrDist = segments[0] === "src" || segments[0] === "dist";
|
|
209
|
+
|
|
210
|
+
// Check if current file is index.*
|
|
211
|
+
currentIsIndex = segments.length === 1 && /^index\.(mjs|cjs|js|ts)$/.test(segments[0]);
|
|
191
212
|
}
|
|
213
|
+
|
|
214
|
+
// Flag 1: Check if we've exited src/dist (was in, now not in)
|
|
215
|
+
if (previousWasInSrcOrDist && !currentIsInSrcOrDist) {
|
|
216
|
+
exitedSrcOrDist = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Flag 2: Check if we've exited index.* (was index, now not index)
|
|
220
|
+
if (previousWasIndex && !currentIsIndex) {
|
|
221
|
+
// eslint-disable-next-line no-unused-vars -- see the declaration above
|
|
222
|
+
exitedIndex = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// If we've exited src/dist, we've found the caller (index.mjs might be optimized away)
|
|
226
|
+
if (exitedSrcOrDist) {
|
|
227
|
+
return f;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update previous state for next iteration
|
|
231
|
+
previousWasInSrcOrDist = currentIsInSrcOrDist;
|
|
232
|
+
previousWasIndex = currentIsIndex;
|
|
192
233
|
}
|
|
193
234
|
return null;
|
|
194
235
|
}
|
|
195
|
-
|
|
196
236
|
/**
|
|
197
237
|
* @function pickFallbackBaseFile
|
|
198
238
|
* @internal
|
|
@@ -222,13 +262,12 @@ function pickPrimaryBaseFile() {
|
|
|
222
262
|
* // - Direct API usage without slothlet loader
|
|
223
263
|
*/
|
|
224
264
|
function pickFallbackBaseFile() {
|
|
265
|
+
// Use simpler fallback logic - just find first non-Node.js-internal file
|
|
225
266
|
for (const cs of getStack(pickFallbackBaseFile)) {
|
|
226
267
|
const f = toFsPath(cs?.getFileName?.());
|
|
227
268
|
if (!f) continue;
|
|
228
269
|
if (f.startsWith?.("node:internal")) continue;
|
|
229
270
|
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
271
|
return f;
|
|
233
272
|
}
|
|
234
273
|
return THIS_FILE;
|
package/src/wisp.mjs
CHANGED
|
@@ -90,14 +90,15 @@ export async function wisp(input, options = {}) {
|
|
|
90
90
|
try {
|
|
91
91
|
validate(val);
|
|
92
92
|
} catch (e) {
|
|
93
|
-
throw new Error(`@cldmv/wisp: ${e?.message ?? e}
|
|
93
|
+
throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e });
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
return val;
|
|
97
97
|
} catch {}
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
|
|
100
|
+
// Legacy import assertions (`assert`) for Node 16.14-20.9, which predate `with`; the cast keeps the type checker from rejecting the key.
|
|
101
|
+
const mod = await import(url.href, /** @type {any} */ ({ assert: { type } }));
|
|
101
102
|
if (!reviver && !validate) return mod?.default ?? mod;
|
|
102
103
|
let val = deepClone(mod?.default ?? mod);
|
|
103
104
|
if (reviver) val = JSON.parse(JSON.stringify(val), reviver);
|
|
@@ -105,7 +106,7 @@ export async function wisp(input, options = {}) {
|
|
|
105
106
|
try {
|
|
106
107
|
validate(val);
|
|
107
108
|
} catch (e) {
|
|
108
|
-
throw new Error(`@cldmv/wisp: ${e?.message ?? e}
|
|
109
|
+
throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e });
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
return val;
|
|
@@ -119,7 +120,7 @@ export async function wisp(input, options = {}) {
|
|
|
119
120
|
try {
|
|
120
121
|
validate(val);
|
|
121
122
|
} catch (e) {
|
|
122
|
-
throw new Error(`@cldmv/wisp: ${e?.message ?? e}
|
|
123
|
+
throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e });
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
return val;
|
|
@@ -127,7 +128,7 @@ export async function wisp(input, options = {}) {
|
|
|
127
128
|
if (fallback) {
|
|
128
129
|
return wisp(fallback, options);
|
|
129
130
|
}
|
|
130
|
-
throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}
|
|
131
|
+
throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`, { cause: e });
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -154,7 +155,7 @@ export async function wisp(input, options = {}) {
|
|
|
154
155
|
* const data = wispSync('./config.json');
|
|
155
156
|
*/
|
|
156
157
|
export function wispSync(input, options = {}) {
|
|
157
|
-
const { base, validate, reviver,
|
|
158
|
+
const { base, validate, reviver, fallback } = options;
|
|
158
159
|
let url;
|
|
159
160
|
if (input instanceof URL) url = input;
|
|
160
161
|
else {
|
|
@@ -172,7 +173,7 @@ export function wispSync(input, options = {}) {
|
|
|
172
173
|
try {
|
|
173
174
|
validate(val);
|
|
174
175
|
} catch (e) {
|
|
175
|
-
throw new Error(`@cldmv/wisp: ${e?.message ?? e}
|
|
176
|
+
throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e });
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
return val;
|
|
@@ -180,7 +181,7 @@ export function wispSync(input, options = {}) {
|
|
|
180
181
|
if (fallback) {
|
|
181
182
|
return wispSync(fallback, options);
|
|
182
183
|
}
|
|
183
|
-
throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}
|
|
184
|
+
throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`, { cause: e });
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -1 +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;
|
|
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;AA4MD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,2CAzCW,MAAM,GACJ,MAAM,CAsDlB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0CA1CW,MAAM,GACJ,MAAM,CAuDlB;AAtXM,4BAvBI,GAAG,GACD,MAAM,GAAC,IAAI,CAsB+F;;iBA0XzG,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"}
|
package/types/wisp.d.mts.map
CHANGED
|
@@ -1 +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,
|
|
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,CA0EtB;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"}
|