Skip to content

release: v1.0.7 - require the ESM entry directly and fail clearly on… - #31

Merged
cldmv-bot[bot] merged 32 commits into
masterfrom
next
Oct 5, 2026
Merged

cldmv-bot[bot] merged 32 commits into
masterfrom
next

Conversation

@cldmv-bot

@cldmv-bot cldmv-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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)

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, fixes #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, fixes #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, fixes #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), and the LICENSE file now carries the Apache-2.0 text.


📦 Packaging

  • Built dist/ output (#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, 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) — 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) — 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) — 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/ — 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): added @cldmv/vitest-runner (^1.5.3), vitest (^5.0.3) and @vitest/coverage-v8 (^5.0.3); removed mocha and chai.
  • Build (#40): added tsup (^8.5.1).
  • Lint and format (#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 took it to ^2.1.4, #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). 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 and #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.
👥 Contributors

coverage

Metric Coverage
Statements 100.0%
Branches 100.0%
Functions 100.0%
Lines 100.0%

Avg: 100.0% · 16885ad · Node lts/*

Co-authored-by: Shinrai Shinrai@users.noreply.github.com

Shinrai and others added 3 commits October 3, 2026 10:31
…hout 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.
@cldmv-bot cldmv-bot Bot added ! release → master v4 flow: persistent next → master release PR (carries the next feature release) release Marks a pull request as a pending release — merge to publish a new version semver: patch This release contains only backwards-compatible bug fixes type: bug Something is broken or not behaving as expected labels Oct 3, 2026
@cldmv-bot

cldmv-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

🔒 Dependency Review

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses
  • ✅ 0 denied package(s)
  • ⚠️ 14 package(s) with OpenSSF Scorecard score < 3 (first 100 deps checked)
    • @jridgewell/resolve-uri@3.1.2 — score 2.8
    • any-promise@1.3.0 — score 2.1
    • ccount@2.0.1 — score 2.5
    • character-entities@2.0.2 — score 2.5
    • decode-named-character-reference@1.3.0 — score 2.9
    • estree-walker@3.0.3 — score 2.1
    • fault@2.0.1 — score 2.5
    • format@0.2.2 — score 2
    • github-slugger@2.0.0 — score 2.9
    • joycon@3.1.1 — score 2.8
    • js-tokens@10.0.0 — score 2.9
    • longest-streak@3.1.0 — score 2.5
    • markdown-table@3.0.4 — score 2.8
    • mz@2.7.0 — score 2.9

Full job summary

@cldmv-bot cldmv-bot Bot added area: tests Touches test files, fixtures, or test infrastructure type: dependencies Relates to dependency updates, version bumps, or package management labels Oct 3, 2026
@cldmv-bot

cldmv-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

⚠️ Bundle size increased

File Raw Δ Raw Gzipped Δ Gzipped
dist/index.cjs 1.8 kB +1.8 kB (+100.0%) ⚠️ 977 B +977 B
dist/index.mjs 3.5 kB +3.5 kB (+100.0%) ⚠️ 1.6 kB +1.6 kB
Total 5.4 kB +5.4 kB 2.5 kB +2.5 kB

📊 Generated by bundle-size. Brotli sizes also measured but omitted from the table for brevity.

Shinrai and others added 3 commits October 3, 2026 17:16
Replace the MIT license with the Apache License 2.0 in LICENSE, package.json and the README.
…rsed

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
@cldmv-bot cldmv-bot Bot added the area: core Touches core library / runtime source code label Oct 4, 2026
@cldmv-bot cldmv-bot Bot added the type: documentation Relates to docs, README updates, guides, or inline code comments label Oct 4, 2026
Shinrai and others added 10 commits October 3, 2026 18:40
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.
The fallback row grew in the v1.0.7 notes update; reformat with the
standard prettier config that #36 brings to wisp, so format:check passes
once both land.
Replace mocha + chai with the fleet-standard vitest setup:

- Port test/wisp.spec.mjs to tests/wisp.test.vitest.mjs (same 23 cases,
  same assertions) and move the fixtures to tests/fixtures/. The node:test
  CommonJS entry check moves to tests/cjs/entry.test.cjs and still runs
  after vitest in `test` and `coverage`.
- Add .configs/vitest.config.mjs and tests/run-vitest.mjs. The config
  disables Vite's module runner (experimental.viteModuleRunner/nodeLoader)
  so src/ is loaded by Node's own import(): the module runner ignores
  import attributes and loads JSON itself, which would test Vite instead
  of wisp's with/assert/fs strategies. Coverage covers src/ and both entry
  points, with a json-summary reporter for the badge.
- Add characterization tests for input forms, base resolution, the legacy
  assert strategy, the fs fallback and its errors, the structuredClone
  fallback, the caller resolver's frame selection, and the index.cjs
  version guard. Coverage is 100% lines/statements/branches/functions.
- Mark the resolveWith non-string guard as unreachable (both wrappers call
  path.isAbsolute first, which already throws).
- Scripts: test, test:watch, coverage, ci:coverage; drop mocha and chai.
- ESLint test globals move from mocha on test/**/*.mjs to the vitest set
  on tests/**/*.test.vitest.mjs; fixture ignores follow the move.
- ci.yml: enable the coverage badge and PR coverage comment, and build the
  coverage legs with `npm run build:types` (wisp ships src/ directly).
… cached

The legacy assert strategy is reached by importing an unsupported type, which
'with' rejects; on Node.js 24+ the assert retry of the same URL then loads it.
Node.js 20 and 22 cache the failed import, so the retry fails with the same
error and wisp reports the unsupported type. CI's Node.js 22.12 leg failed
those five tests. They now run on Node.js 24+, and older versions check the
unsupported-type error instead.
Shinrai and others added 3 commits October 3, 2026 19:54
Ship a bundled dist/ instead of src/, matching the fleet-standard CLDMV
library build:

- tsup bundles src/index.mjs into dist/index.mjs (ESM, minified,
  keepNames, node: specifiers kept). Import attributes are marked
  supported so the `with` / `assert` runtime probes in src/wisp.mjs reach
  Node unchanged instead of being stripped for the node16 target.
- dist/index.cjs is not a second bundle: src/cjs-shim.cjs (the former
  root index.cjs, unchanged in behaviour) is copied in verbatim and loads
  ./index.mjs through require(esm).
- The root index.mjs / index.cjs entries move to src/index.mjs and
  src/cjs-shim.cjs. package.json exports point at dist/ (plus types), with
  a `wisp-dev` condition serving src/; `files` ships dist/, types/,
  README and LICENSE only. dist/ is gitignored and rebuilt by prepack.
- Scripts: build = tsup, types:build / types:check replace build:types /
  test:types' direct tsc call, build:ci = lint + format:check +
  types:build + build + tests + type check, test:cjs builds first.
- The node:test CJS check now runs against dist/, and a new
  tests/bundle/caller-resolution.test.mjs runs fixture callers that
  import / require the built dist/ from another directory and checks
  relative paths resolve against the caller, not dist/ (and that JSON is
  loaded through import attributes).
- CI / release / publish workflows build with types:build + build;
  coverage legs use build:ci; bundle-size measures dist/.

The public export shape is unchanged for import (default, wisp,
wispSync) and require (the function with .default, .wisp, .wispSync).
…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
@cldmv-bot cldmv-bot Bot added type: ci Changes to CI workflows, actions, or build pipelines type: config Changes to repository or project configuration files labels Oct 4, 2026
Shinrai and others added 12 commits October 3, 2026 20:01
… 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.
Bumps the dev dependency to 2.1.4. npm run fix:headers scanned 44 files
and restamped none.
Bumps @cldmv/fix-headers from 2.1.4 to 2.2.0. No file headers changed.
Raises the range from ^1.2.1 (locked 1.2.1) to ^1.2.4. Configs 1.2.4 sets forceAuthorUpdate and forceLastModifiedAuthorUpdate to false, so with fix-headers 2.2.0 @author is never rewritten and @last modified by changes only on real content edits. Ran fix:headers under the new config: 0 files restamped.
@cldmv-bot
cldmv-bot Bot merged commit 4f7f995 into master Oct 5, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Touches core library / runtime source code area: tests Touches test files, fixtures, or test infrastructure ! release → master v4 flow: persistent next → master release PR (carries the next feature release) release Marks a pull request as a pending release — merge to publish a new version semver: patch This release contains only backwards-compatible bug fixes type: bug Something is broken or not behaving as expected type: ci Changes to CI workflows, actions, or build pipelines type: config Changes to repository or project configuration files type: dependencies Relates to dependency updates, version bumps, or package management type: documentation Relates to docs, README updates, guides, or inline code comments

Projects

None yet

1 participant