From 3a81ce13fe2081227ea3e0371ec8c1eb8e3db172 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 10:31:45 -0700 Subject: [PATCH 01/20] fix(cjs): require the ESM entry directly and fail clearly on Node without require(esm) index.cjs used createRequire(__filename) to load index.mjs, which breaks esbuild/webpack bundling. A .cjs file already has require() in scope, so swap to a plain require("./index.mjs") instead. Also add a version check in index.cjs: Node.js versions without require(esm) (before 20.19 / 22.12) now get a clear ERR_REQUIRE_ESM message pointing at import() instead of a bare loader error. tests/cjs equivalent (test/entry.test.cjs): node:test checks run by `npm test` after Mocha: require() returns the same objects as import, and the version check fires when require(esm) is off. --- index.cjs | 20 ++++++++++++----- package.json | 3 ++- test/entry.test.cjs | 54 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 test/entry.test.cjs diff --git a/index.cjs b/index.cjs index cda135d..7e9945a 100644 --- a/index.cjs +++ b/index.cjs @@ -19,8 +19,9 @@ * @public * * @description - * This module provides CommonJS exports for the wisp and wispSync functions. - * It uses createRequire to load the ESM implementation and re-exports it. + * This module is a thin wrapper: it loads index.mjs through Node's synchronous require(esm) + * and re-exports it. It uses a plain require() rather than createRequire so the file keeps + * working when bundled by esbuild/webpack. * * @example * const { wisp, wispSync } = require('@cldmv/wisp'); @@ -29,9 +30,18 @@ "use strict"; -const { createRequire } = require("module"); -const requireESM = createRequire(__filename); -const esm = requireESM("./index.mjs"); +// index.cjs is a thin wrapper: it loads index.mjs through Node's synchronous require(esm). +// Node.js versions without require(esm) would fail with a bare ERR_REQUIRE_ESM, so fail +// early with a message that says what to do instead. +if (!process.features?.require_module) { + const error = new Error( + `@cldmv/wisp: require() needs Node.js ^20.19.0 or >=22.12.0 (this is ${process.version}). On older Node.js, load the package with import() instead.` + ); + error.code = "ERR_REQUIRE_ESM"; + throw error; +} + +const esm = require("./index.mjs"); module.exports = esm.default; module.exports.default = esm.default; diff --git a/package.json b/package.json index b331203..d97b9fc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ } }, "scripts": { - "test": "mocha --recursive \"test/**/*.mjs\"", + "test": "mocha --recursive \"test/**/*.mjs\" && npm run test:cjs", + "test:cjs": "node --test test/entry.test.cjs", "test:watch": "mocha --watch \"test/**/*.mjs\"", "lint": "eslint --config .configs/eslint.config.mjs .", "build:types": "tsc", diff --git a/test/entry.test.cjs b/test/entry.test.cjs new file mode 100644 index 0000000..b338f88 --- /dev/null +++ b/test/entry.test.cjs @@ -0,0 +1,54 @@ +/** + * + * @Project: @cldmv/wisp + * @Filename: /test/entry.test.cjs + * @Date: 2026-10-03T10:24:25-07:00 (1791048265) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T10:24:25-07:00 (1791048265) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * CommonJS entry tests. These run under Node's own test runner (`node --test`), not Mocha: + * Mocha's `test/**\/*.mjs` glob only picks up ESM specs, so it cannot show whether a plain + * `require()` of the package works the way it does for a CommonJS consumer. + */ +"use strict"; + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); + +const repoRoot = path.resolve(__dirname, ".."); + +test("require() returns the same wisp object as import", async () => { + const cjs = require("../index.cjs"); + const esm = await import("../index.mjs"); + + assert.equal(cjs, esm.default); + assert.equal(cjs.default, esm.default); + assert.equal(cjs.wisp, esm.wisp); + assert.equal(cjs.wispSync, esm.wispSync); + assert.equal(typeof cjs.wisp, "function"); + assert.equal(typeof cjs.wispSync, "function"); +}); + +test("require() fails with a clear message where Node.js has no require(esm)", () => { + // --no-experimental-require-module turns require(esm) off, which is what Node.js + // versions before 20.19 / 22.12 look like to the entry. + const res = spawnSync(process.execPath, ["--no-experimental-require-module", "-e", "require('./index.cjs')"], { + cwd: repoRoot, + encoding: "utf8" + }); + + assert.notEqual(res.status, 0); + assert.match(res.stderr, /ERR_REQUIRE_ESM/); + assert.match(res.stderr, /require\(\) needs Node\.js \^20\.19\.0 or >=22\.12\.0/); + assert.match(res.stderr, /import\(\)/); +}); From 517d19e4d4297f524c811a4de12f98ad7ded9757 Mon Sep 17 00:00:00 2001 From: "cldmv-bot[bot]" <230771808+cldmv-bot[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:09:19 +0000 Subject: [PATCH 02/20] chore: bump version to 1.0.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd129d9..3f157cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/wisp", - "version": "1.0.6", + "version": "1.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/wisp", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "devDependencies": { "@cldmv/configs": "^1.2.1", diff --git a/package.json b/package.json index d97b9fc..3e35034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/wisp", - "version": "1.0.6", + "version": "1.0.7", "description": "Version-agnostic JSON importing for Node.js with fallback handling and caller path resolution.", "type": "module", "exports": { From 10e7cf72390ed26227e5f9246129c22f0ed9e44b Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 17:16:16 -0700 Subject: [PATCH 03/20] chore: relicense under Apache-2.0 Replace the MIT license with the Apache License 2.0 in LICENSE, package.json and the README. --- LICENSE | 223 ++++++++++++++++++++++++++++++++++++++++++++++----- README.md | 2 +- package.json | 2 +- 3 files changed, 204 insertions(+), 23 deletions(-) diff --git a/LICENSE b/LICENSE index 091a8ab..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License - -Copyright (c) 2025 CLDMV Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 2dbdef3..26f53b5 100644 --- a/README.md +++ b/README.md @@ -150,4 +150,4 @@ try { ## License -MIT © CLDMV Inc. +Apache-2.0 © CLDMV Inc. See [LICENSE](https://github.com/CLDMV/wisp/blob/master/LICENSE) for the full text. diff --git a/package.json b/package.json index 3e35034..e082e00 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "url": "https://github.com/CLDMV/wisp/issues" }, "homepage": "https://github.com/CLDMV/wisp#readme", - "license": "MIT", + "license": "Apache-2.0", "funding": { "type": "github", "url": "https://github.com/sponsors/shinrai" From 23ea828e10f84c129782895059a927155f22f25e Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 17:33:47 -0700 Subject: [PATCH 04/20] fix: use the fallback file only when the primary cannot be read or parsed A primary that loads but fails the caller's validate function now throws the validation error instead of silently loading the fallback. The fallback is not given a further fallback, which also fixes infinite recursion when the fallback itself failed to load or validate. Fixes #33 --- src/wisp.mjs | 51 ++++++++++++++++++++-------------- test/fixtures/invalid.json | 1 + test/wisp.spec.mjs | 56 ++++++++++++++++++++++++++++++++++++++ types/wisp.d.mts.map | 2 +- 4 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 test/fixtures/invalid.json diff --git a/src/wisp.mjs b/src/wisp.mjs index d2945b3..ba3d790 100644 --- a/src/wisp.mjs +++ b/src/wisp.mjs @@ -51,6 +51,23 @@ function deepClone(v) { return typeof globalThis.structuredClone === "function" ? globalThis.structuredClone(v) : JSON.parse(JSON.stringify(v)); } +/** + * Runs the caller's validation on a loaded value, throwing the wisp-prefixed load error when it rejects the data. + * @private + * @param {((val: any) => void)|undefined} validate - Validation function, if any. + * @param {*} val - The parsed JSON value. + * @param {URL} url - The URL the value was loaded from. + * @returns {void} + */ +function runValidate(validate, val, url) { + if (!validate) return; + try { + validate(val); + } catch (e) { + throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: @cldmv/wisp: ${e?.message ?? e}`, { cause: e }); + } +} + /** * Asynchronously loads JSON from a file, trying modern import with 'with', then 'assert', then fs. * @public @@ -115,23 +132,20 @@ export async function wisp(input, options = {}) { } catch {} if (type === "json") { + let val; try { const txt = await readFile(url, "utf8"); - const val = deepClone(JSON.parse(txt, reviver)); - if (validate) { - try { - validate(val); - } catch (e) { - throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e }); - } - } - return val; + val = deepClone(JSON.parse(txt, reviver)); } catch (e) { + // Only a primary that cannot be read or parsed falls through to the fallback; the fallback itself gets no further fallback. if (fallback) { - return wisp(fallback, options); + return wisp(fallback, { ...options, fallback: undefined }); } throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`, { cause: e }); } + // A validation failure on a loaded primary is the caller's rejection, never a reason to use the fallback. + runValidate(validate, val, url); + return val; } throw new Error(`@cldmv/wisp: Unsupported type '${type}' or failed to load module at ${url.href}`); @@ -168,23 +182,20 @@ export function wispSync(input, options = {}) { else url = new URL(resolveUrlFromCaller(s)); } + let val; try { const txt = fs.readFileSync(url, "utf8"); - const val = deepClone(JSON.parse(txt, reviver)); - if (validate) { - try { - validate(val); - } catch (e) { - throw new Error(`@cldmv/wisp: ${e?.message ?? e}`, { cause: e }); - } - } - return val; + val = deepClone(JSON.parse(txt, reviver)); } catch (e) { + // Only a primary that cannot be read or parsed falls through to the fallback; the fallback itself gets no further fallback. if (fallback) { - return wispSync(fallback, options); + return wispSync(fallback, { ...options, fallback: undefined }); } throw new Error(`@cldmv/wisp: Failed to load JSON file at ${url.href}: ${e.message}`, { cause: e }); } + // A validation failure on a loaded primary is the caller's rejection, never a reason to use the fallback. + runValidate(validate, val, url); + return val; } export default wisp; diff --git a/test/fixtures/invalid.json b/test/fixtures/invalid.json new file mode 100644 index 0000000..50fa57c --- /dev/null +++ b/test/fixtures/invalid.json @@ -0,0 +1 @@ +{ "broken": diff --git a/test/wisp.spec.mjs b/test/wisp.spec.mjs index 3e4392f..654c178 100644 --- a/test/wisp.spec.mjs +++ b/test/wisp.spec.mjs @@ -85,6 +85,39 @@ describe("wisp", () => { expect(data.foo).to.equal("bar"); }); + it("does not use the fallback when the primary fails validation", async () => { + const validate = (val) => { + if (val.foo) throw new Error("primary rejected"); + }; + let err; + try { + await wisp(path.resolve(testDir, "fixtures/sample.json"), { validate, fallback: path.resolve(testDir, "fixtures/nested/ok.json") }); + } catch (e) { + err = e; + } + expect(err, "expected the validation error to be thrown").to.be.an("error"); + expect(err.message).to.match(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); + }); + + it("uses the fallback when the primary is invalid JSON", async () => { + const data = await wisp(path.resolve(testDir, "fixtures/invalid.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); + expect(data.foo).to.equal("bar"); + }); + + it("validates the fallback data when the primary is missing", async () => { + const validate = () => { + throw new Error("fallback rejected"); + }; + let err; + try { + await wisp(path.resolve(testDir, "fixtures/nonexistent.json"), { validate, fallback: path.resolve(testDir, "fixtures/sample.json") }); + } catch (e) { + err = e; + } + expect(err, "expected the validation error to be thrown").to.be.an("error"); + expect(err.message).to.include("@cldmv/wisp: fallback rejected"); + }); + it("resolves caller path correctly", async () => { const callerPath = path.resolve(testDir, "fixtures/caller.js"); const data = await wisp(callerPath); @@ -132,6 +165,29 @@ describe("wispSync", () => { expect(data.foo).to.equal("bar"); }); + it("does not use the fallback when the primary fails validation in sync", () => { + const validate = (val) => { + if (val.foo) throw new Error("primary rejected"); + }; + expect(() => + wispSync(path.resolve(testDir, "fixtures/sample.json"), { validate, fallback: path.resolve(testDir, "fixtures/nested/ok.json") }) + ).to.throw(/^@cldmv\/wisp: Failed to load JSON file at .*sample\.json: @cldmv\/wisp: primary rejected$/); + }); + + it("uses the fallback when the primary is invalid JSON in sync", () => { + const data = wispSync(path.resolve(testDir, "fixtures/invalid.json"), { fallback: path.resolve(testDir, "fixtures/sample.json") }); + expect(data.foo).to.equal("bar"); + }); + + it("validates the fallback data when the primary is missing in sync", () => { + const validate = () => { + throw new Error("fallback rejected"); + }; + expect(() => + wispSync(path.resolve(testDir, "fixtures/nonexistent.json"), { validate, fallback: path.resolve(testDir, "fixtures/sample.json") }) + ).to.throw("@cldmv/wisp: fallback rejected"); + }); + it("resolves caller path correctly in sync", () => { const callerPath = path.resolve(testDir, "fixtures/caller.js"); const data = wispSync(callerPath); diff --git a/types/wisp.d.mts.map b/types/wisp.d.mts.map index 57b4cc3..7d6f57b 100644 --- a/types/wisp.d.mts.map +++ b/types/wisp.d.mts.map @@ -1 +1 @@ -{"version":3,"file":"wisp.d.mts","sourceRoot":"","sources":["../src/wisp.mjs"],"names":[],"mappings":"AAqDA;;;;;;;;;;;;;;;;;;;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"} \ No newline at end of file +{"version":3,"file":"wisp.d.mts","sourceRoot":"","sources":["../src/wisp.mjs"],"names":[],"mappings":"AAsEA;;;;;;;;;;;;;;;;;;;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,CAuEtB;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,CAmCb"} \ No newline at end of file From 54d61ebde800869ea24dca34f735033dea0e9960 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 16:48:09 -0700 Subject: [PATCH 05/20] =?UTF-8?q?docs(changelog):=20backfill=20v1.0.0?= =?UTF-8?q?=E2=80=93v1.0.6=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/v1/v1.0.0.md | 34 +++++++++++++++++++++++++++ docs/changelog/v1/v1.0.1.md | 31 ++++++++++++++++++++++++ docs/changelog/v1/v1.0.2.md | 25 ++++++++++++++++++++ docs/changelog/v1/v1.0.3.md | 24 +++++++++++++++++++ docs/changelog/v1/v1.0.4.md | 47 +++++++++++++++++++++++++++++++++++++ docs/changelog/v1/v1.0.5.md | 35 +++++++++++++++++++++++++++ docs/changelog/v1/v1.0.6.md | 30 +++++++++++++++++++++++ 7 files changed, 226 insertions(+) create mode 100644 docs/changelog/v1/v1.0.0.md create mode 100644 docs/changelog/v1/v1.0.1.md create mode 100644 docs/changelog/v1/v1.0.2.md create mode 100644 docs/changelog/v1/v1.0.3.md create mode 100644 docs/changelog/v1/v1.0.4.md create mode 100644 docs/changelog/v1/v1.0.5.md create mode 100644 docs/changelog/v1/v1.0.6.md diff --git a/docs/changelog/v1/v1.0.0.md b/docs/changelog/v1/v1.0.0.md new file mode 100644 index 0000000..8e00066 --- /dev/null +++ b/docs/changelog/v1/v1.0.0.md @@ -0,0 +1,34 @@ +# Wisp v1.0.0 Changelog + +**Release Date**: November 2025 +**Release Type**: Major (initial release) + +--- + +## Overview + +First public release of `@cldmv/wisp`, a small library for loading JSON files in Node.js without caring which JSON import syntax the running Node.js version supports. + +--- + +## ✨ Features + +### Version-agnostic JSON loading + +- `wisp(input, options)` loads a JSON file asynchronously. It tries `import(url, { with: { type: "json" } })` first, then the legacy `import(url, { assert: { type: "json" } })`, and finally reads and parses the file with `fs.readFile`. +- `wispSync(input, options)` loads a JSON file synchronously with `fs.readFileSync`. +- `input` accepts a relative path, an absolute path, a `file://` string or a `URL`. Relative paths resolve from the file that called `wisp` / `wispSync`, unless `options.base` is given. +- Options: `base` (base path or URL for relative inputs), `validate` (called with the parsed value; a throw rejects the load), `reviver` (passed to `JSON.parse`), `type` (import attribute type, `"json"` by default, async only) and `fallback` (a second path or URL to try when the first one cannot be loaded). +- Values returned with a `reviver` or `validate` are deep-cloned (with `structuredClone` when available), so the module cache is never mutated. + +### Packaging + +- Dual entry points: `index.mjs` for `import` and `index.cjs` for `require()`, with `wisp` as the default export. +- Declared `engines.node` of `>=16.14`. + +--- + +## Upgrade notes + +- Initial release; nothing to upgrade from. +- Relative-path resolution from the caller's location does not work correctly in this version when the package is installed as a dependency (paths resolve from inside the package). Upgrade to [v1.0.1](./v1.0.1.md). diff --git a/docs/changelog/v1/v1.0.1.md b/docs/changelog/v1/v1.0.1.md new file mode 100644 index 0000000..6cf9fc2 --- /dev/null +++ b/docs/changelog/v1/v1.0.1.md @@ -0,0 +1,31 @@ +# Wisp v1.0.1 Changelog + +**Release Date**: November 2025 +**Release Type**: Patch + +--- + +## Overview + +Fixes caller path resolution, which made relative paths unusable from any package that depended on `@cldmv/wisp`, and starts shipping the type declarations. + +--- + +## 🐛 Bug Fixes + +### Relative paths resolve from the caller again + +In v1.0.0, `wispSync("../examples/data.json")` called from a consuming module resolved the path relative to `node_modules/@cldmv/wisp/src/wisp.mjs` instead of the caller, and failed with `ENOENT`. The resolver in `src/lib/resolve-from-caller.mjs` was rewritten: it finds the package root by walking up to the nearest `package.json`, walks the call stack, and picks the first frame after the stack leaves the package's `src/` or `dist/` directory. It no longer depends on hard-coded file names or on an `index.mjs` frame being present (Node.js does not create one for `export *` re-exports). The bug and the fix are described in [BUGS.md](https://github.com/CLDMV/wisp/blob/master/BUGS.md). + +--- + +## 📦 Packaging + +- The `types/` folder (generated `.d.mts` declarations) is now included in the published package. +- `engines.node` lowered from `>=16.14` to `>=16.0.0`; on Node.js before 16.14 the file-system fallback is used. + +--- + +## Upgrade notes + +- No API changes — drop-in for v1.0.0. Code that worked around the path bug by passing an absolute path or `base` keeps working. diff --git a/docs/changelog/v1/v1.0.2.md b/docs/changelog/v1/v1.0.2.md new file mode 100644 index 0000000..805f2d2 --- /dev/null +++ b/docs/changelog/v1/v1.0.2.md @@ -0,0 +1,25 @@ +# Wisp v1.0.2 Changelog + +**Release Date**: July 2026 +**Release Type**: Patch + +--- + +## Overview + +Moves the repository onto the CLDMV v4 staging-branch release flow. No runtime code changed. + +This version was tagged on GitHub but was not published to npm; npm went from v1.0.1 to v1.0.5. + +--- + +## 🔧 CI & tooling + +- Replaced the old release workflow with the CLDMV v4 workflow set: work merges into `next`, a persistent release PR carries it to `master`, and `hotfixes` handles urgent fixes ([#2](https://github.com/CLDMV/wisp/pull/2)). +- Added CodeQL, OpenSSF Scorecard, dependency review, CLA, labeler, PR-title normalization, stale, branch-retention, tag-health and release-notification workflows. + +--- + +## Upgrade notes + +- No runtime change — drop-in for v1.0.1. diff --git a/docs/changelog/v1/v1.0.3.md b/docs/changelog/v1/v1.0.3.md new file mode 100644 index 0000000..8fe85f1 --- /dev/null +++ b/docs/changelog/v1/v1.0.3.md @@ -0,0 +1,24 @@ +# Wisp v1.0.3 Changelog + +**Release Date**: August 2026 +**Release Type**: Patch + +--- + +## Overview + +A development-dependency security update. No runtime code changed. + +This version was tagged on GitHub but was not published to npm; npm went from v1.0.1 to v1.0.5. + +--- + +## 🔧 Dependencies + +- `picomatch` 2.3.1 → 2.3.2, a development-only transitive dependency of `mocha` ([#3](https://github.com/CLDMV/wisp/pull/3), [#4](https://github.com/CLDMV/wisp/pull/4)). + +--- + +## Upgrade notes + +- No runtime change — drop-in for v1.0.2. diff --git a/docs/changelog/v1/v1.0.4.md b/docs/changelog/v1/v1.0.4.md new file mode 100644 index 0000000..8bda855 --- /dev/null +++ b/docs/changelog/v1/v1.0.4.md @@ -0,0 +1,47 @@ +# Wisp v1.0.4 Changelog + +**Release Date**: September 2026 +**Release Type**: Patch + +--- + +## Overview + +Wires up ESLint for the first time, keeps the original error attached to the errors wisp throws, and moves the test toolchain to mocha 12. + +This version was tagged on GitHub but was not published to npm; npm went from v1.0.1 to v1.0.5. + +--- + +## 🐛 Bug Fixes + +### Errors keep their original cause + +Errors thrown by `wisp` and `wispSync` (validation failures and "Failed to load JSON file" errors) now pass the underlying error as `cause`, so `error.cause` holds the original `ENOENT`, `SyntaxError` or validation error. The messages are unchanged. + +### Lint cleanup + +`wispSync` no longer destructures the `type` option it never used, and an unused variable was removed from the caller resolver. Neither changes behavior. + +--- + +## 🔧 CI & tooling + +- ESLint is now actually configured (`.configs/eslint.config.mjs`); the `lint` script previously pointed at a missing setup. +- Added `.github/dependabot.yml` targeting `next`, the `feature-pr.yml` auto-PR workflow, and moved the release and feature-PR workflows to the current thin-caller templates ([#6](https://github.com/CLDMV/wisp/pull/6)). +- The hotfix redirector now uses the v4 reusable workflow and signs its security cherry-picks ([#8](https://github.com/CLDMV/wisp/pull/8)). +- The CI Node.js matrix now runs from 22.12.0 up to 26 ([#10](https://github.com/CLDMV/wisp/pull/10)). + +--- + +## 🔧 Dependencies + +- `mocha` ^10.2.0 → ^12.0.1, which fixes ESM/CJS interop on Node.js 26 and removes a large set of old transitive dependencies ([#9](https://github.com/CLDMV/wisp/pull/9), [#10](https://github.com/CLDMV/wisp/pull/10)). +- Development dependency group update ([#5](https://github.com/CLDMV/wisp/pull/5)). +- Added `eslint`, `@eslint/js` and `globals` as development dependencies. + +--- + +## Upgrade notes + +- No API changes — drop-in for v1.0.3. Code that inspects `error.cause` now gets the original error instead of `undefined`. diff --git a/docs/changelog/v1/v1.0.5.md b/docs/changelog/v1/v1.0.5.md new file mode 100644 index 0000000..710e50d --- /dev/null +++ b/docs/changelog/v1/v1.0.5.md @@ -0,0 +1,35 @@ +# Wisp v1.0.5 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch + +--- + +## Overview + +Development toolchain updates (TypeScript 6, chai 6, `@types/node` 26) and v4 workflow syncs. The only source change is a type-checker cast; runtime behavior is unchanged. This is the first npm release since v1.0.1, so it also delivers the changes from v1.0.2–v1.0.4 to npm. + +--- + +## 🔧 CI & tooling + +- Synced the v4 workflows with the CLDMV/.github v4.29.2 templates ([#20](https://github.com/CLDMV/wisp/pull/20)), added the bundle-size workflow measuring the published JS files ([#22](https://github.com/CLDMV/wisp/pull/22)), and stopped a skipped PR-run mirror job from satisfying the required PR check ([#24](https://github.com/CLDMV/wisp/pull/24)). +- `tsconfig.json` moved to `module` / `moduleResolution` `NodeNext` for TypeScript 6. The legacy `import(…, { assert })` call in `src/wisp.mjs` gained a type cast so the checker accepts the `assert` key; the call itself is unchanged. + +--- + +## 🔧 Dependencies + +- `typescript` ^5.2.0 → ^6.0.3 ([#21](https://github.com/CLDMV/wisp/pull/21)) +- `chai` 4.5.0 → 6.2.2 ([#15](https://github.com/CLDMV/wisp/pull/15)) +- `@types/node` 20.19.24 → 26.6.3 ([#18](https://github.com/CLDMV/wisp/pull/18), [#23](https://github.com/CLDMV/wisp/pull/23)) +- `eslint` 10.10.0 → 10.11.0 ([#17](https://github.com/CLDMV/wisp/pull/17)) +- `mocha` 12.0.1 → 12.0.2 ([#16](https://github.com/CLDMV/wisp/pull/16)) + +All are development dependencies; the package still has no runtime dependencies. + +--- + +## Upgrade notes + +- No runtime change — drop-in for v1.0.1 (the previous npm release). See [v1.0.4](./v1.0.4.md) for the `error.cause` addition that reaches npm with this version. diff --git a/docs/changelog/v1/v1.0.6.md b/docs/changelog/v1/v1.0.6.md new file mode 100644 index 0000000..0ae98df --- /dev/null +++ b/docs/changelog/v1/v1.0.6.md @@ -0,0 +1,30 @@ +# Wisp v1.0.6 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch + +--- + +## Overview + +Uniform file headers, a CI fix and a development-dependency security update. No runtime code changed; the published `.mjs` / `.cjs` files differ only in their header comments. + +--- + +## 🔧 CI & tooling + +- Adopted the shared CLDMV `@cldmv/fix-headers` config (`npm run fix:headers`) and stamped uniform file headers across the source and workflow files ([#28](https://github.com/CLDMV/wisp/pull/28)). +- The in-repo PR mirror job now runs instead of being skipped ([#29](https://github.com/CLDMV/wisp/pull/29)). + +--- + +## 🔧 Dependencies + +- `brace-expansion` 5.0.9 → 5.0.12 and `serialize-javascript` 7.1.1 → 7.1.2, development-only transitive dependencies ([#26](https://github.com/CLDMV/wisp/pull/26)). +- Added `@cldmv/fix-headers` and `@cldmv/configs` as development dependencies. + +--- + +## Upgrade notes + +- No runtime change — drop-in for v1.0.5. From eb14e699237b366b49fea77f725694b85dc969d3 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 16:49:10 -0700 Subject: [PATCH 06/20] docs: v1.0.7 release notes and README restructure --- README.md | 166 ++++++++++++++++++++++++++++++------ docs/changelog/v1/v1.0.7.md | 48 +++++++++++ 2 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 docs/changelog/v1/v1.0.7.md diff --git a/README.md b/README.md index 26f53b5..e528e8d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,46 @@ # @cldmv/wisp -A Node.js module for version-agnostic JSON importing, providing transparent support for modern and legacy import syntaxes with automatic fallbacks. +**@cldmv/wisp** loads JSON files in Node.js without caring which JSON import syntax the running Node.js version understands. It tries the modern `import ... with { type: "json" }` form first, falls back to the legacy `assert` form, and finally reads and parses the file itself, so the same call works from Node.js 16 through the current release. -## Overview +Relative paths resolve from the file that calls wisp, not from wisp's own location, so `wispSync("./config.json")` means what it looks like it means from anywhere in your project — including from packages that depend on wisp. -`@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. +> _Load JSON the same way on every Node.js version — quietly, like a wisp._ -## Node.js Version Support +[![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] [![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] [![npm last update]][npm_last_update_url] + +[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url] + +--- + +## ✨ What's New + +### Latest: v1.0.7 (October 2026) + +- **`require()` works in bundles and fails clearly on older Node.js** — `index.cjs` now loads the ESM entry with a plain `require("./index.mjs")` instead of `createRequire(__filename)`, so `require("@cldmv/wisp")` survives esbuild and webpack bundling. On Node.js versions without synchronous `require(esm)`, where `require()` never worked, it now throws an `ERR_REQUIRE_ESM` error that names the supported versions (`^20.19.0` or `>=22.12.0`) and points to `import()`. The ESM entry and the library code are unchanged (#30). +- [View full v1.0.7 Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.7.md) + +### Recent Releases + +- **v1.0.6** (October 2026) — Uniform file headers via `@cldmv/fix-headers`, a CI fix and a development-dependency security update; no runtime change ([Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.6.md)) +- **v1.0.5** (October 2026) — First npm release since v1.0.1; TypeScript 6, chai 6 and `@types/node` 26 for development, v4 workflow syncs; no runtime change ([Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.5.md)) +- **v1.0.4** (September 2026) — Thrown errors now carry the original error as `cause`; ESLint wired up; mocha 12 ([Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.4.md)) +- **v1.0.3** (August 2026) — Development-dependency security update (`picomatch`); no runtime change ([Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.3.md)) + +📚 **For complete version history and detailed release notes, see the [docs/changelog/](https://github.com/CLDMV/wisp/tree/master/docs/changelog/) folder.** + +--- + +## 🚀 Key Features + +- **Version-agnostic JSON imports** — `with`, then `assert`, then a file-system read; whichever the running Node.js supports. +- **Caller-aware paths** — relative paths resolve from the calling file; `base` overrides it. +- **Async and sync** — `wisp()` returns a promise, `wispSync()` returns the value directly. +- **Validation and revivers** — `validate` rejects bad data, `reviver` is passed to `JSON.parse`. +- **Fallback files** — `fallback` names a second file to try when the first cannot be loaded. +- **ESM and CommonJS** — `import` and `require()` entry points, with TypeScript declarations included. +- **Zero runtime dependencies.** + +### Node.js Version Support | Node Version | `import ... with { type: 'json' }` | `import ... assert { type: 'json' }` | Fallback | | ------------ | ---------------------------------- | ------------------------------------ | -------- | @@ -16,15 +50,26 @@ A Node.js module for version-agnostic JSON importing, providing transparent supp | ≥ 16.14 | ❌ | ✅ | ✅ | | < 16.14 | ❌ | ❌ | ✅ | -## Installation +--- + +## 📦 Installation + +### Requirements + +- **Node.js 16 or higher** for `import` (ESM). +- **`require()` needs Node.js ^20.19.0 or >=22.12.0** (synchronous `require(esm)`). On older Node.js versions, load the package with `import()` instead. + +### Install ```bash npm install @cldmv/wisp ``` -## Usage +--- + +## 🚀 Quick Start -### ESM (Modern) +### ESM ```javascript import { wisp, wispSync } from "@cldmv/wisp"; @@ -36,7 +81,7 @@ const config = await wisp("./config.json"); const data = wispSync("./data.json"); ``` -### CJS (CommonJS) +### CommonJS ```javascript const { wisp, wispSync } = require("@cldmv/wisp"); @@ -50,7 +95,9 @@ wisp("./config.json").then((config) => { const data = wispSync("./data.json"); ``` -## API Reference +--- + +## 📖 API Reference ### `wisp(input, options?)` @@ -63,6 +110,8 @@ Asynchronously loads JSON from a file. - `base` (string | URL, optional): Base URL for resolving relative paths. Defaults to the caller's file URL. - `validate` (function, optional): Validation function called with the parsed JSON. Throws if validation fails. - `reviver` (function, optional): Reviver function passed to `JSON.parse`. + - `type` (string, optional): Import attribute type used for the `import()` attempts. Defaults to `"json"`; the file-system fallback only runs for `"json"`. + - `fallback` (string | URL, optional): A second file to load when `input` cannot be loaded. #### Returns @@ -88,11 +137,11 @@ Synchronously loads JSON from a file. #### Parameters - `input` (string | URL): Path or URL to the JSON file -- `options` (object, optional): Same as `wisp` options. +- `options` (object, optional): Same as `wisp` options, except `type` (the file is always read and parsed as JSON). #### Returns -`Promise<*>`: The parsed JSON value. +`*`: The parsed JSON value. #### Example @@ -106,15 +155,21 @@ const data = wispSync("./config.json", { }); ``` -## Options +--- -| Option | Type | Description | -| ---------- | ---------- | ------------------------------------------------------------------------ | -| `base` | string/URL | Base URL for relative path resolution. Defaults to caller's file URL. | -| `validate` | function | Validation function. Receives parsed JSON, should throw on invalid data. | -| `reviver` | function | JSON.parse reviver function for custom parsing. | +## ⚙️ Options -## Path Resolution +| Option | Type | Description | +| ---------- | ---------- | ----------------------------------------------------------------------------------- | +| `base` | string/URL | Base URL for relative path resolution. Defaults to caller's file URL. | +| `validate` | function | Validation function. Receives parsed JSON, should throw on invalid data. | +| `reviver` | function | JSON.parse reviver function for custom parsing. | +| `type` | string | Import attribute type for `wisp()`'s `import()` attempts. Defaults to `"json"`. | +| `fallback` | string/URL | File to load instead when `input` cannot be loaded (missing, unreadable, invalid). | + +--- + +## 🧭 Path Resolution `@cldmv/wisp` uses caller-aware path resolution: @@ -122,7 +177,9 @@ const data = wispSync("./config.json", { - Absolute paths and URLs are used as-is - The `base` option overrides the default caller-based resolution -## Fallback Order +--- + +## 🔁 Fallback Order The module attempts to load JSON in this order: @@ -130,11 +187,13 @@ The module attempts to load JSON in this order: 2. `import(url, { assert: { type: 'json' } })` (Node ≥ 16.14) 3. `fs.readFile` / `fs.readFileSync` (all supported Node versions) -This ensures maximum compatibility across Node.js versions. +This ensures maximum compatibility across Node.js versions. `wispSync` always uses `fs.readFileSync`. -## Error Handling +--- -Validation errors are prefixed with `@cldmv/wisp:` for easy identification: +## 🛡 Error Handling + +Errors thrown by wisp are prefixed with `@cldmv/wisp:` for easy identification, and carry the underlying error as `error.cause`. A validation failure is reported as part of the load error: ```javascript try { @@ -144,10 +203,69 @@ try { } }); } catch (error) { - console.log(error.message); // "@cldmv/wisp: Custom validation failed" + console.log(error.message); // "@cldmv/wisp: Failed to load JSON file at file:///…/invalid.json: @cldmv/wisp: Custom validation failed" } ``` -## License +--- + +## 📚 Documentation + +- **[Changelog](https://github.com/CLDMV/wisp/tree/master/docs/changelog/)** — release notes for every version +- **[Bug reports and fixes](https://github.com/CLDMV/wisp/blob/master/BUGS.md)** — write-ups of notable bugs and how they were fixed + +[![CodeFactor]][codefactor_url] [![OpenSSF Scorecard]][ossf_scorecard_url] [![npms.io score]][npms_url] [![npm unpacked size]][npm_size_url] [![Repo size]][repo_size_url] + +--- + +## 🤝 Contributing + +Contributions are welcome — open an [issue](https://github.com/CLDMV/wisp/issues) or a pull request. + +[![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url] + +--- + +## 🔗 Links + +- **npm**: [@cldmv/wisp](https://www.npmjs.com/package/@cldmv/wisp) +- **GitHub**: [CLDMV/wisp](https://github.com/CLDMV/wisp) +- **Issues**: [GitHub Issues](https://github.com/CLDMV/wisp/issues) +- **Changelog**: [docs/changelog/](https://github.com/CLDMV/wisp/tree/master/docs/changelog/) + +--- + +## 📄 License + +[![GitHub license]][github_license_url] [![npm license]][npm_license_url] Apache-2.0 © CLDMV Inc. See [LICENSE](https://github.com/CLDMV/wisp/blob/master/LICENSE) for the full text. + +[npm version]: https://img.shields.io/npm/v/%40cldmv%2Fwisp.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_version_url]: https://www.npmjs.com/package/@cldmv/wisp +[last commit]: https://img.shields.io/github/last-commit/CLDMV/wisp?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[last_commit_url]: https://github.com/CLDMV/wisp/commits +[npm last update]: https://img.shields.io/npm/last-update/%40cldmv%2Fwisp?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_last_update_url]: https://www.npmjs.com/package/@cldmv/wisp +[codefactor]: https://img.shields.io/codefactor/grade/github/CLDMV/wisp?style=for-the-badge&logo=codefactor&logoColor=white&labelColor=F44A6A +[codefactor_url]: https://www.codefactor.io/repository/github/cldmv/wisp +[openssf scorecard]: https://img.shields.io/ossf-scorecard/github.com/CLDMV/wisp?style=for-the-badge&label=OpenSSF%20Scorecard +[ossf_scorecard_url]: https://scorecard.dev/viewer/?uri=github.com/CLDMV/wisp +[npms.io score]: https://img.shields.io/npms-io/final-score/%40cldmv%2Fwisp?style=for-the-badge&logo=npms&logoColor=white&labelColor=0B5D57 +[npms_url]: https://npms.io/search?q=%40cldmv%2Fwisp +[npm downloads]: https://img.shields.io/npm/dm/%40cldmv%2Fwisp.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_downloads_url]: https://www.npmjs.com/package/@cldmv/wisp +[github downloads]: https://img.shields.io/github/downloads/CLDMV/wisp/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[github_downloads_url]: https://github.com/CLDMV/wisp/releases +[npm unpacked size]: https://img.shields.io/npm/unpacked-size/%40cldmv%2Fwisp.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_size_url]: https://www.npmjs.com/package/@cldmv/wisp +[repo size]: https://img.shields.io/github/repo-size/CLDMV/wisp?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[repo_size_url]: https://github.com/CLDMV/wisp +[github license]: https://img.shields.io/github/license/CLDMV/wisp.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[github_license_url]: https://github.com/CLDMV/wisp/blob/HEAD/LICENSE +[npm license]: https://img.shields.io/npm/l/%40cldmv%2Fwisp.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837 +[npm_license_url]: https://www.npmjs.com/package/@cldmv/wisp +[contributors]: https://img.shields.io/github/contributors/CLDMV/wisp.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 +[contributors_url]: https://github.com/CLDMV/wisp/graphs/contributors +[sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor +[sponsor_url]: https://github.com/sponsors/shinrai diff --git a/docs/changelog/v1/v1.0.7.md b/docs/changelog/v1/v1.0.7.md new file mode 100644 index 0000000..cd4911b --- /dev/null +++ b/docs/changelog/v1/v1.0.7.md @@ -0,0 +1,48 @@ +# Wisp v1.0.7 Changelog + +**Release Date**: October 2026 +**Release Type**: Patch +**Branch**: `release/1.0.7` + +--- + +## Overview + +Fixes the CommonJS entry point so `require("@cldmv/wisp")` works inside esbuild and webpack bundles, and makes `require()` fail with a clear message on Node.js versions that cannot load ES modules synchronously. The ESM entry and the library code are unchanged. + +--- + +## 🐛 Bug Fixes + +### `require()` works in bundles and fails clearly on older Node.js ([#30](https://github.com/CLDMV/wisp/pull/30)) + +`index.cjs` loaded `index.mjs` through `createRequire(__filename)`, which bundlers such as esbuild and webpack cannot follow. It now uses the plain `require("./index.mjs")` that a `.cjs` file already has in scope. + +`index.cjs` has always depended on Node.js's synchronous `require(esm)`, so `require("@cldmv/wisp")` never worked on Node.js versions without it. Those versions used to fail with a bare loader error; `index.cjs` now checks `process.features.require_module` first and throws an `ERR_REQUIRE_ESM` error that names the supported versions (`^20.19.0` or `>=22.12.0`) and points to `import()` instead. `import("@cldmv/wisp")` keeps working on every Node.js version the package supports. + +The exports are the same as before: `require("@cldmv/wisp")` returns `wisp`, with `.default`, `.wisp` and `.wispSync` attached. + +--- + +## 🔧 CI & tooling + +- New `test/entry.test.cjs` (run by `npm test` through `npm run test:cjs`) checks that `require()` returns the same functions as `import`, and that the version check fires when `require(esm)` is unavailable. + +--- + +## 📚 Documentation + +- **NEW:** [docs/changelog/v1/](https://github.com/CLDMV/wisp/tree/master/docs/changelog/v1) — per-version changelogs for every release from v1.0.0. +- README reorganized; the error-handling example now shows the actual error message, and the `wispSync` return type and the `type` / `fallback` options are documented. + +--- + +## 🔧 Dependencies + +_No dependency updates_ + +--- + +## Upgrade notes + +- No breaking changes — drop-in for v1.0.6. On Node.js versions without `require(esm)` (anything outside `^20.19.0` or `>=22.12.0`), `require()` still fails as it always did, now with an explanatory message; use `import()` there. From 808b5f8b555909d6371ad123016baa0b9883e4b3 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 18:41:20 -0700 Subject: [PATCH 07/20] docs: cover #35 and #34 in the v1.0.7 notes and README The validation/fallback fix and the Apache-2.0 relicense merged into next and ship in v1.0.7. Describe both, add the behaviour change to the upgrade notes, and make the fallback option docs say a validate failure throws. --- README.md | 5 +++-- docs/changelog/v1/v1.0.7.md | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e528e8d..3083f85 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Relative paths resolve from the file that calls wisp, not from wisp's own locati ### Latest: v1.0.7 (October 2026) - **`require()` works in bundles and fails clearly on older Node.js** — `index.cjs` now loads the ESM entry with a plain `require("./index.mjs")` instead of `createRequire(__filename)`, so `require("@cldmv/wisp")` survives esbuild and webpack bundling. On Node.js versions without synchronous `require(esm)`, where `require()` never worked, it now throws an `ERR_REQUIRE_ESM` error that names the supported versions (`^20.19.0` or `>=22.12.0`) and points to `import()`. The ESM entry and the library code are unchanged (#30). +- **A failed validation throws instead of loading the fallback** — `fallback` is now used only when the primary file cannot be read or parsed; a `validate` rejection is reported as an error, and a fallback that also fails no longer loops forever ([#35](https://github.com/CLDMV/wisp/pull/35)). The package is also relicensed under Apache-2.0 ([#34](https://github.com/CLDMV/wisp/pull/34)). - [View full v1.0.7 Changelog](https://github.com/CLDMV/wisp/blob/master/docs/changelog/v1/v1.0.7.md) ### Recent Releases @@ -111,7 +112,7 @@ Asynchronously loads JSON from a file. - `validate` (function, optional): Validation function called with the parsed JSON. Throws if validation fails. - `reviver` (function, optional): Reviver function passed to `JSON.parse`. - `type` (string, optional): Import attribute type used for the `import()` attempts. Defaults to `"json"`; the file-system fallback only runs for `"json"`. - - `fallback` (string | URL, optional): A second file to load when `input` cannot be loaded. + - `fallback` (string | URL, optional): A second file to load when `input` cannot be read or parsed. A `validate` failure on `input` throws rather than falling back. #### Returns @@ -165,7 +166,7 @@ const data = wispSync("./config.json", { | `validate` | function | Validation function. Receives parsed JSON, should throw on invalid data. | | `reviver` | function | JSON.parse reviver function for custom parsing. | | `type` | string | Import attribute type for `wisp()`'s `import()` attempts. Defaults to `"json"`. | -| `fallback` | string/URL | File to load instead when `input` cannot be loaded (missing, unreadable, invalid). | +| `fallback` | string/URL | File to load instead when `input` cannot be read or parsed (missing, unreadable, or not valid JSON). A `validate` failure throws instead. | --- diff --git a/docs/changelog/v1/v1.0.7.md b/docs/changelog/v1/v1.0.7.md index cd4911b..711863e 100644 --- a/docs/changelog/v1/v1.0.7.md +++ b/docs/changelog/v1/v1.0.7.md @@ -8,7 +8,7 @@ ## Overview -Fixes the CommonJS entry point so `require("@cldmv/wisp")` works inside esbuild and webpack bundles, and makes `require()` fail with a clear message on Node.js versions that cannot load ES modules synchronously. The ESM entry and the library code are unchanged. +Fixes the CommonJS entry point so `require("@cldmv/wisp")` works inside esbuild and webpack bundles, and makes `require()` fail with a clear message on Node.js versions that cannot load ES modules synchronously. It also stops a failed `validate` check from silently loading the `fallback` file, fixes an endless loop when the fallback itself could not be loaded, and relicenses the package under Apache-2.0. --- @@ -22,10 +22,23 @@ Fixes the CommonJS entry point so `require("@cldmv/wisp")` works inside esbuild The exports are the same as before: `require("@cldmv/wisp")` returns `wisp`, with `.default`, `.wisp` and `.wispSync` attached. +### A failed validation throws instead of loading the fallback ([#35](https://github.com/CLDMV/wisp/pull/35), fixes [#33](https://github.com/CLDMV/wisp/issues/33)) + +`wisp` and `wispSync` ran `validate` inside the same `try` block as reading and parsing the file, so when the primary file loaded fine but the caller's `validate` rejected it, wisp fell through to `fallback` as if the file were missing. The data the caller asked to reject was silently replaced by the fallback's. The fallback is now used only when the primary file cannot be read or parsed (missing, unreadable, or not valid JSON). A validation failure throws, in the same format as before: `Failed to load JSON file at : @cldmv/wisp: `. + +The same change fixes a second bug: the fallback was loaded with the same options, `fallback` included, so a fallback that also failed to load or validate retried itself forever. `wispSync` ended with `Maximum call stack size exceeded` and `wisp` never settled. The fallback is now loaded without a further fallback and throws if it fails. + +--- + +## 📄 License + +The package is relicensed from MIT to Apache-2.0 ([#34](https://github.com/CLDMV/wisp/pull/34)), and the `LICENSE` file now carries the Apache-2.0 text. + --- ## 🔧 CI & tooling +- New tests for `wisp` and `wispSync`: a primary that fails validation throws without using the fallback, a primary that is invalid JSON uses the fallback, and a fallback that fails validation throws. - New `test/entry.test.cjs` (run by `npm test` through `npm run test:cjs`) checks that `require()` returns the same functions as `import`, and that the version check fires when `require(esm)` is unavailable. --- @@ -45,4 +58,6 @@ _No dependency updates_ ## Upgrade notes -- No breaking changes — drop-in for v1.0.6. On Node.js versions without `require(esm)` (anything outside `^20.19.0` or `>=22.12.0`), `require()` still fails as it always did, now with an explanatory message; use `import()` there. +- **Behaviour change:** if you relied on a failed `validate` check falling back to the `fallback` file, it now throws instead. Catch the error and load the fallback yourself if that is what you want. +- On Node.js versions without `require(esm)` (anything outside `^20.19.0` or `>=22.12.0`), `require()` still fails as it always did, now with an explanatory message; use `import()` there. +- The license is now Apache-2.0. From cb130f2242392c3f123a097c227b43461e866fc4 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 3 Oct 2026 18:47:31 -0700 Subject: [PATCH 08/20] chore: add standard lint/format setup (prettier, eslint, precommit hook) --- .configs/.prettierrc | 29 + .configs/eslint.config.mjs | 63 +- .githooks/install.mjs | 63 ++ .githooks/pre-commit | 26 + .prettierignore | 18 + BUGS.md | 6 +- package-lock.json | 1485 ++++++++++++++++++++++++++++++++++-- package.json | 19 +- 8 files changed, 1628 insertions(+), 81 deletions(-) create mode 100644 .configs/.prettierrc create mode 100644 .githooks/install.mjs create mode 100755 .githooks/pre-commit create mode 100644 .prettierignore diff --git a/.configs/.prettierrc b/.configs/.prettierrc new file mode 100644 index 0000000..f84f51b --- /dev/null +++ b/.configs/.prettierrc @@ -0,0 +1,29 @@ +{ + "useTabs": true, + "tabWidth": 2, + "printWidth": 140, + "trailingComma": "none", + "quoteProps": "preserve", + "bracketSpacing": true, + "overrides": [ + { + "files": "*.xml", + "options": { + "printWidth": 300 + } + }, + { + "files": "*.md", + "options": { + "proseWrap": "preserve" + } + }, + { + "files": "*.jsonv", + "options": { + "parser": "jsonv" + } + } + ], + "plugins": ["@cldmv/prettier-plugin-jsonv"] +} diff --git a/.configs/eslint.config.mjs b/.configs/eslint.config.mjs index eab033b..63429eb 100644 --- a/.configs/eslint.config.mjs +++ b/.configs/eslint.config.mjs @@ -2,12 +2,12 @@ * * @Project: @cldmv/wisp * @Filename: /.configs/eslint.config.mjs - * @Date: 2026-09-13T15:59:51-07:00 (1789340391) + * @Date: 2026-08-27T08:03:34-07:00 (1787843014) * @Author: Nate Corcoran * @Email: * ----- * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) - * @Last modified time: 2026-10-02T15:12:03-07:00 (1790979123) + * @Last modified time: 2026-10-03T11:39:13-07:00 (1791052753) * ----- * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. * @@ -15,23 +15,42 @@ import js from "@eslint/js"; import globals from "globals"; +import json from "@eslint/json"; +import jsonvPlugin from "@cldmv/eslint-plugin-jsonv"; +import markdown from "@eslint/markdown"; +import css from "@eslint/css"; import { defineConfig } from "eslint/config"; export default defineConfig([ { ignores: [ + "tmp/**", + "trash/**", "node_modules/**", "dist/**", + "build/**", + ".git/**", + ".configs/**", + ".vscode/**", "coverage/**", - "tmp/**", - "trash/**", - "**/package-lock.json", + "reference/**", "*.min.*", - // Test fixtures are deliberately-non-JS data files that happen to - // carry a .js extension (wisp's own fallback-loading tests exercise - // exactly this "not really parseable as a module" case) -- they're - // test data, not source, and were never meant to be linted as JS. - "test/fixtures/**" + // Deliberately-non-JS data files with a .js extension (fallback-loading tests); test data, not source. + "test/fixtures/**", + "**/package-lock.json", + // Copy file patterns + "*copy/", + "*copy (*)/", + "*copy */", + "*copy.*", + "*copy (*).*", + "*copy *.*", + "**/*copy/", + "**/*copy (*)/", + "**/*copy */", + "**/*copy.*", + "**/*copy (*).*", + "**/*copy *.*" ] }, { @@ -48,12 +67,26 @@ export default defineConfig([ varsIgnorePattern: "^(_|___.*)$" } ], - // wisp.mjs's multi-strategy import fallback (try `with`, then - // `assert`, then fs.readFile) deliberately swallows each earlier - // strategy's failure with an empty catch before trying the next. + // wisp's multi-strategy import fallback deliberately swallows each earlier strategy's failure with an empty catch. "no-empty": ["error", { allowEmptyCatch: true }] } }, - { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.node } } }, - { files: ["test/**/*.mjs"], languageOptions: { globals: { ...globals.mocha } } } + { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: { ...globals.node, ...globals.browser } } }, + { files: ["test/**/*.mjs"], languageOptions: { globals: { ...globals.mocha } } }, + { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, + { files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc", extends: ["json/recommended"] }, + { files: ["**/*.json5"], plugins: { json }, language: "json/json5", extends: ["json/recommended"] }, + { files: ["**/*.jsonv"], plugins: { jsonv: jsonvPlugin }, language: "jsonv/jsonv", ...jsonvPlugin.configs.recommended }, + { + files: ["**/*.md"], + plugins: { markdown }, + language: "markdown/gfm", + extends: ["markdown/recommended"], + rules: { + // GitHub alerts like [!NOTE]/[!WARNING] are valid but trip this rule. + "markdown/no-missing-label-refs": "off" + } + }, + { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] } ]); diff --git a/.githooks/install.mjs b/.githooks/install.mjs new file mode 100644 index 0000000..0d51682 --- /dev/null +++ b/.githooks/install.mjs @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * + * @Project: @cldmv/wisp + * @Filename: /.githooks/install.mjs + * @Date: 2026-10-03T11:23:18-07:00 (1791051798) + * @Author: Nate Corcoran + * @Email: + * ----- + * @Last modified by: Nate Corcoran (Shinrai@users.noreply.github.com) + * @Last modified time: 2026-10-03T11:39:15-07:00 (1791052755) + * ----- + * @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved. + * + */ + +/** + * @fileoverview Installs the committed pre-commit hook into `.git/hooks/pre-commit`. + * + * Wire it into package.json so it runs on `npm install`. Use the guarded form + * below — NOT a bare `node .githooks/install.mjs`: `prepare` also runs on + * `npm pack` / `npm publish` against the packed tree, where `.githooks/` is + * excluded from `files`, so a bare invocation fails module resolution and + * aborts the publish before the guards below can run. + * "scripts": { "prepare": "node -e \"import('./.githooks/install.mjs').catch(()=>{})\"" } + * Copy this file + `pre-commit` (from CLDMV/.github examples/git-hooks/) into + * the repo's `.githooks/` directory. + * + * Why copy into `.git/hooks` rather than set `core.hooksPath`: a per-repo + * `core.hooksPath` SHADOWS a global `core.hooksPath` dispatcher, silently + * disabling any global commit policy (no-coauthor / no-unsigned-push) for that + * repo. A global dispatcher instead CHAINS to `.git/hooks/`, so installing + * here composes with global policy instead of replacing it. + * + * Guards (each exits 0 — install is best-effort, never fails a build): + * - CI: nothing commits on CI, skip. + * - inside node_modules: this package installed as a dependency, skip. + * - no `.git` dir: tarball / shallow export / worktree pointer, skip. + */ +import { existsSync, mkdirSync, copyFileSync, chmodSync, statSync } from "node:fs"; +import { dirname, join, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, ".."); + +if (process.env.CI) process.exit(0); +if (repoRoot.split(sep).includes("node_modules")) process.exit(0); + +const gitDir = join(repoRoot, ".git"); +if (!existsSync(gitDir) || !statSync(gitDir).isDirectory()) process.exit(0); + +const hooksDir = join(gitDir, "hooks"); +mkdirSync(hooksDir, { recursive: true }); + +const dest = join(hooksDir, "pre-commit"); +copyFileSync(join(here, "pre-commit"), dest); +try { + chmodSync(dest, 0o755); +} catch { + /* Windows has no executable bit — ignore. */ +} +console.log("✓ installed .git/hooks/pre-commit (CLDMV lint/format gate)"); diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..cc0711b --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,26 @@ +#!/bin/sh +# +# CLDMV fleet pre-commit hook. +# +# Runs the repo's lint + format checks before a commit, then any repo-local +# `precommit:local` checks. Each uses `npm run