@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.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 src/",
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
- "mocha": "^10.2.0",
68
- "chai": "^4.3.10",
69
- "typescript": "^5.2.0",
70
- "@types/node": "^20.8.0"
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
- const THIS_DIR = path.dirname(THIS_FILE);
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
- // 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.
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
- * 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.
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. 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
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 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
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
- const files = [];
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
- files.push(f);
179
- }
197
+ if (f === THIS_FILE) continue;
180
198
 
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];
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
- const mod = await import(url.href, { assert: { type } });
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, type = "json", fallback } = options;
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;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"}
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"}
@@ -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,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"}
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"}