Repository navigation
fix: revive a namespace without a default export, and report a non-JSON validate rejection - #41
Merged
Merged
Conversation
…ON validate rejection Both bugs live in the two import() strategy blocks of wisp(), so they are fixed together. A reviver or validate on a module with no default export passed the module namespace to structuredClone, which cannot clone a namespace object. The DataCloneError was swallowed as a failed strategy and the load ended in the generic "Unsupported type" error. wisp now copies the namespace's own enumerable exports to a plain object first, consistent with returning the namespace itself when no options are given; a reviver gets that copy through JSON.parse, which already returns a fresh value. wispSync only parses JSON and never sees a namespace, so it is unaffected. validate ran inside each import() strategy's try block, so a rejection counted as a strategy failure: the next strategy re-imported the module and validated again, and a non-JSON type ended in "Unsupported type". validate now runs after the strategy loop on every path and throws the same error as the fs path, "Failed to load JSON file at <url>: @cldmv/wisp: <message>", with the original error as the cause. A rejecting validate is now called once instead of once per strategy. Fixes #38 Fixes #39
Shinrai
force-pushed
the
fix/38-39-import-reviver-validate
branch
from
October 4, 2026 02:58
e9b4c60 to
e944f48
Compare
… loop CodeQL's js/unused-loop-variable flagged the for...of variable, which was only used as import()'s options argument. Loading through a small helper makes the use explicit; behaviour is unchanged.
Shinrai
approved these changes
Oct 4, 2026
This was referenced Oct 4, 2026
cldmv-bot Bot
added a commit
that referenced
this pull request
Oct 5, 2026
# 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. 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.
Two more `wisp()` fixes for modules loaded through `import()`: a `reviver` or `validate` on a module without a default export now works instead of ending in "Unsupported type", and a `validate` rejection on any `import()` path throws the validation error once.
The published package is now built: `src/` is bundled with tsup into `dist/index.mjs`, `dist/index.cjs` is a thin `require()` wrapper around it, and the tarball ships only `dist/`, `types/`, `README.md` and `LICENSE`. The `@cldmv/wisp` entry points and their exports are unchanged; only code that reached into the package's files directly is affected (see Upgrade notes). The test suite moved from mocha to `@cldmv/vitest-runner` with 100% coverage, and the standard lint/format setup was added.
---
## 🐛 Bug Fixes
### `require()` works in bundles and fails clearly on older Node.js ([#30](#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. The CommonJS entry now ships as `dist/index.cjs` (see Packaging below).
### A failed validation throws instead of loading the fallback ([#35](#35), fixes [#33](#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 <url>: @cldmv/wisp: <message>`.
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.
### `reviver` and `validate` work on a module without a default export ([#41](#41), fixes [#38](#38))
When `wisp()` loaded a module through `import()` and was given a `reviver` or `validate`, it copied the module's value with `structuredClone` before handing it over. For a module with no default export that value is the module namespace object, which `structuredClone` cannot copy. The resulting `DataCloneError` was swallowed as a failed import strategy, and the call ended in the generic `Unsupported type '<type>' or failed to load module at <url>` error. `wisp()` now copies the namespace's exports to a plain object first, so `reviver` and `validate` receive a plain-object copy of the module's exports. Without either option the namespace itself is still returned, as before. `wispSync` only parses JSON files and is not affected.
### A `validate` rejection on an `import()` path throws the validation error once ([#41](#41), fixes [#39](#39))
`validate` ran inside each `import()` strategy's `try` block, so a rejection counted as that strategy failing: the next strategy imported the module again and validated it again. For `type: "json"` the call ended on the file-system path and threw the validation error there, after calling `validate` up to three times; for any other `type` it ended in `Unsupported type`, hiding the validation error entirely. `validate` now runs once, after the module has loaded, and a rejection throws the same error as the file-system path, `Failed to load JSON file at <url>: @cldmv/wisp: <message>`, with the original error as `cause`. The same release refactors the strategy loop to clear a CodeQL `js/unused-loop-variable` finding; behaviour is unchanged by that part.
---
## 📄 License
The package is relicensed from MIT to Apache-2.0 ([#34](#34)), and the `LICENSE` file now carries the Apache-2.0 text.
---
## 📦 Packaging
- **Built `dist/` output** ([#40](#40)) — tsup bundles `src/index.mjs` into a minified `dist/index.mjs` (target Node.js 16, function names kept). `dist/index.cjs` is not a second bundle: it is the CommonJS wrapper from [#30](#30), copied in verbatim, and loads `./index.mjs` through `require(esm)`. The root `index.mjs` / `index.cjs` files are gone (the sources now live at `src/index.mjs` and `src/cjs-shim.cjs`).
- **`files`** now lists only `dist/` (without sourcemaps), `types/`, `README.md` and `LICENSE`. `src/` and the root entry files are no longer published.
- **`exports`** — `import` resolves to `dist/index.mjs`, `require` to `dist/index.cjs`, and a `types` condition plus top-level `main`, `module` and `types` fields point at the built files and at `types/index.d.mts`. A `wisp-dev` condition serves `src/` directly for local development.
- Caller-relative path resolution is checked against the built `dist/` output, for both `import` and `require()`, by the new `tests/bundle/caller-resolution.test.mjs`.
---
## 🔧 CI & tooling
- **Test suite on `@cldmv/vitest-runner`** ([#37](#37)) — the mocha + chai suite under `test/` moved to vitest under `tests/` (`*.test.vitest.mjs`), with new tests for the import strategies and the caller resolver. Coverage is 100% and the coverage badge and PR coverage comment are enabled in CI. The legacy `assert`-strategy tests run on Node.js 24+, because Node.js 20 and 22 cache a failed `import()` of the same URL; older versions check the unsupported-type error instead.
- **Standard lint/format setup** ([#36](#36)) — prettier and ESLint configs in `.configs/`, a `.prettierignore`, `lint` / `lint:fix` / `format` / `format:check` scripts, and a check-only pre-commit hook installed by `prepare`.
- **Build scripts** ([#40](#40)) — `build` runs tsup, `types:build` / `types:check` replace `build:types`, `prepack` builds types and `dist/` before packing, and `build:ci` runs lint, format check, types, build and tests. CI, publish and release workflows build `dist/` alongside the types, the coverage jobs run `build:ci` before measuring, and the bundle-size check measures `dist/index.mjs` and `dist/index.cjs`.
- `BUGS.md` was reformatted by prettier (code-fence languages and spacing; no content change), and one `/* v8 ignore next */` comment marks a type check in `src/lib/resolve-from-caller.mjs` that the exported wrappers make unreachable. Neither changes behaviour.
- 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 `tests/cjs/entry.test.cjs` (run by `npm test` through `npm run test:cjs`, which now builds `dist/` first) 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 runtime dependencies were added or changed; the package still has none, and `engines.node` stays `>=16.0.0`. All changes are to development dependencies:
- **Test toolchain** ([#37](#37)): added `@cldmv/vitest-runner` (`^1.5.3`), `vitest` (`^5.0.3`) and `@vitest/coverage-v8` (`^5.0.3`); removed `mocha` and `chai`.
- **Build** ([#40](#40)): added `tsup` (`^8.5.1`).
- **Lint and format** ([#36](#36)): added `prettier` (`^3.9.9`), `@eslint/json`, `@eslint/markdown`, `@eslint/css`, `@cldmv/eslint-plugin-jsonv`, `@cldmv/prettier-plugin-jsonv` and `@cldmv/jsonv`; `eslint` `^10.10.0` → `^10.12.0` and `globals` `^17.12.0` → `^17.13.0`.
- **`@cldmv/fix-headers`** `^2.1.2` → `^2.2.0` ([#42](#42) took it to `^2.1.4`, [#44](#44) to `^2.2.0`). Version 2.1.4 no longer writes a JavaScript comment into JSON or Markdown files, processes every repeated `--input`, and never walks dependency folders such as `node_modules`. Version 2.2.0 makes the `@Last modified by` header tag follow content edits only, so a header-only rewrite keeps the recorded editor instead of replacing it.
- **`@cldmv/configs`** `^1.2.1` → `^1.2.4` ([#44](#44)). It provides the shared `fix-headers` configuration that `.configs/fix-headers.json` extends. Version 1.2.4 turns off `forceAuthorUpdate` and `forceLastModifiedAuthorUpdate` (both were on in 1.2.1), so the shared configuration no longer overwrites the recorded author or last editor.
- [#42](#42) and [#44](#44) change only `package.json` and the lockfile; no file headers were restamped.
- **Development Node.js floor.** `vitest` 5 supports `^22.12.0 || ^24.0.0 || >=26.0.0` and `@cldmv/vitest-runner` 1.5.3 and `@cldmv/fix-headers` 2.2.0 require `>=22.12.0`, so contributors need Node.js 22.12 or newer (23 is not supported by the test runner). The CI matrix already started at 22.12.0. The published package still runs on Node.js 16 and later for `import`.
---
## Upgrade notes
- **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.
- **Behaviour change:** a `validate` rejection on a module loaded through `import()` now throws the validation error, called once. Previously a non-JSON `type` reported `Unsupported type` instead, and `validate` could run up to three times for a single load. Code that matched on the `Unsupported type` message for a rejected module should match the validation error instead.
- **Behaviour change:** `wisp()` with a `reviver` or `validate` on a module without a default export now succeeds, and they receive a plain-object copy of the module's exports. Previously that call failed with `Unsupported type`.
- **Packaging change:** the `@cldmv/wisp` entry points (`import` and `require()`) and their exports are unchanged. The package's file layout is not: the root `index.mjs` / `index.cjs` and `src/` are no longer published, and the code ships as `dist/index.mjs` and `dist/index.cjs`. The `exports` map already limited Node.js resolution to `@cldmv/wisp` itself, so subpath imports such as `@cldmv/wisp/src/wisp.mjs` were already rejected; code that bypassed `exports` and loaded files from `node_modules/@cldmv/wisp/` by path (`index.mjs`, `index.cjs`, or anything under `src/`), or a bundler or tool configured to ignore `exports`, has to switch to the `@cldmv/wisp` specifier.
- The license is now Apache-2.0.
<details>
<summary>👥 Contributors</summary>
- @Shinrai
</details>
---
<!-- coverage-start -->

| Metric | Coverage |
|--------|----------|
| Statements | 100.0% |
| Branches | 100.0% |
| Functions | 100.0% |
| Lines | 100.0% |
*Avg: **100.0%** · `16885ad` · Node lts/**
<!-- coverage-end -->
<!-- co-authors -->
Co-authored-by: Shinrai <Shinrai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚀 What's Changed
💥 Breaking Changes
No breaking changes
✨ Features
No new features
🐛 Bug Fixes
📦 Dependencies
No dependency updates
🔧 Other Changes
👥 Contributors