GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — build: validate the published package with publint and are-the-types-wrong by JosunLP · Pull Request #236 · bQuery/bQuery

Conversation

@JosunLP

@JosunLP JosunLP commented Sep 18, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

Summary

  • Adds publint and @arethetypeswrong/cli as check:publish, wired into the PR gate, the publish workflow and prepublishOnly.
  • Running them found a third package problem the audit had missed, which this PR also fixes: every emitted .d.ts used extensionless relative imports — a hard TS2834 for consumers with skipLibCheck: false.

Motivation

Closes #218.

The issue predicted both tools would have surfaced #219 automatically. They did — publint reported all 23 types-ordering errors verbatim. They also found more:

Finding Status
23 × types ordered after import Fixed in #234 (confirmed clean here)
315 × InternalResolutionError — extensionless imports in .d.ts New. Fixed in this PR
repository.url not a full git URL Fixed here
node10 resolution failures, CJS-resolves-to-ESM, false-esm Accepted policy, ignored with reasons

Was stacked on #234, now targets dev. #234 has merged, so this is a plain dev PR again. dev is merged in.

Changes included

  • package.json: publint + @arethetypeswrong/cli dev deps; check:publint, check:attw, check:publish scripts; check:publish added to prepublishOnly; repository.url → git+https://….
  • New scripts/postbuild-types.mjs, run from build:types. Rewrites extensionless relative specifiers in emitted declarations to explicit .js, resolving each against what is actually in dist/ and leaving anything unresolvable alone. Rewrote 628 specifiers across 211 of 284 files.
  • .github/workflows/test.yml: new package job.
  • .github/workflows/npm-publish.yml: check:publish in the build job and before npm publish.
  • New tests/postbuild-types.test.ts — 12 tests over the specifier resolver and the rewriter.
  • docs/contributing/release-process.md: a check:publish section including the ignored-rules table with reasons.

Validation

  • bun run lint — bunx eslint . clean
  • bun run build — rebuilt from clean
  • bun test — 3339 pass, 1 skip, 0 fail
  • Additional focused checks: bun run check:publish exits 0; bun run check, format:check, lint:types, test:types pass

The declaration fix, measured against a real consumer (moduleResolution: nodenext, skipLibCheck: false):

before: dist/index.d.ts(10,15): error TS2834: Relative import paths need explicit
        file extensions ...  (×12 shown, tsc exit 2)
after:  tsc exit 0

attw problem counts on the packed tarball: InternalResolutionError 315 → 0. Runtime re-verified after every change: require('@bquery/bquery') → 638 exports, ESM root and sub-path imports work, require('@bquery/bquery/package.json') resolves.

Checklist

  • I followed the contributing guidelines.
  • I added or updated tests where behavior changed.
  • I updated documentation when public behavior or developer workflow changed.
  • I confirmed the change does not introduce security-sensitive regressions.

Notes for reviewers

Why a declaration fix is in a "add tooling" PR. Adding attw and leaving 315 problems in place would mean either a permanently red gate or ignoring internal-resolution-error — which would make the whole exercise ceremonial. The problem is real and reproduces outside attw, so it is fixed.

Why post-process rather than fix the source. The correct fix is .js extensions on relative imports throughout src/, which touches nearly every file. Only the declarations need it — Vite resolves the runtime bundles fine. The rewrite is conservative: it only appends an extension when the target actually exists in dist/.

The .d.cts question is settled, and not the way this PR originally said. An earlier revision of this description claimed a .d.cts twin produced green tools over broken types, and I left false-esm ignored on that basis. That conclusion came from testing a single copied declaration, which does push TS1479 one level down. A full .d.cts tree with every specifier resolved to .cjs type-checks cleanly — 0 errors both with and without skipLibCheck. That tree is what #234 shipped as scripts/postbuild-cts.mjs, so --ignore-rules false-esm is gone from this PR and publint now reports "All good!".

publint runs without --strict. --strict would turn remaining warnings fatal; suppressing them entirely would hide them.

The package job and #233's quality job coexist in test.yml, as do #241's coverage job. The merge of dev here keeps all three.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
JosunLP and others added 2 commits September 18, 2026 13:54
@JosunLP @claude
fix(build): make require('@bquery/bquery') work and order types f…
89e7fbb
…irst

Investigating the reported `exports` map problems turned up a worse one:
`require('@bquery/bquery')` resolved but returned an empty object. The
package is `"type": "module"` and the UMD bundle shipped as
`dist/full.umd.js`, so Node read it as ESM. The UMD wrapper's CommonJS
branch never ran, and `require()` handed back an empty module namespace
instead of the 638 exports — silently, with no error to go on.

Emit the UMD bundle as `dist/full.umd.cjs` so Node reads it as CommonJS,
and point `main` and both `require` conditions at it. The `unpkg` and
`jsdelivr` fields now point at `dist/full.iife.js`, which is the build
meant for script tags; `dist/full.umd.js` is gone, and the README and
`src/full.ts` examples use the IIFE URL.

Also apply the three fixes the issue asks for: `types` now comes first in
all 23 sub-path entries, `./package.json` is exported, and the ESM-only
policy for sub-paths is documented in the getting-started guide with the
three CommonJS workarounds.

`tests/package-exports.test.ts` pins all of it, including that no
`require` condition may point at a bare `.js` file again.

Closes #219

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@JosunLP @claude
build: validate the published package with publint and are-the-types-…
f229792
…wrong

Add both tools as `check:publint` / `check:attw` / `check:publish`, and run
them from the PR gate, the publish workflow and `prepublishOnly`.

Running them found a third problem beyond the two already known: every
emitted declaration used extensionless relative imports. `tsc` re-emits
specifiers exactly as the source writes them, and this is an ESM package,
so under `moduleResolution: node16`/`nodenext` a consumer with
`skipLibCheck: false` gets a wall of TS2834 — 315 problems by attw's
count. Most projects run `skipLibCheck: true`, which is why it went
unnoticed. `scripts/postbuild-types.mjs` now rewrites the specifiers as
part of `build:types`, taking 315 to 0; a strict `nodenext` consumer with
`skipLibCheck: false` type-checks clean.

Three attw results are ignored on purpose, all following from the
documented ESM-only policy: node10 resolution (predates `exports`),
CJS-resolves-to-ESM on sub-paths, and false-esm on the root. The last one
is a real gap — `require('@bquery/bquery')` works at runtime but its types
are ESM-shaped — and closing it needs a parallel `.d.cts` declaration
tree, not a copied `.d.cts`, which does not typecheck because its
re-exports still point at ESM declarations. publint keeps printing it as a
warning so it stays visible. All of this is recorded in
docs/contributing/release-process.md.

Also fixes publint's one remaining suggestion: `repository.url` is now a
full `git+https://` URL.

Closes #218

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: bf6d8d53-9b30-4b38-9617-b8b3eff12c5b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
  • X
  • Mastodon
  • Reddit
  • LinkedIn

Comment @coderabbitai help to get the list of available commands.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot added docs Changes to the documentation tests Chenges to the tests build Changes to the build and meta files ci/cd Changes to the action workflows labels Sep 18, 2026
@JosunLP @claude
fix(tests): avoid String.prototype.replaceAll in the postbuild-types …
e35537c
…test

`test:types` builds the test tree against tsconfig.component-test.json,
whose lib predates ES2021, so `replaceAll` fails with TS2550. Uses
split/join instead, matching how the repo already avoids newer built-ins
in the test tree.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@JosunLP JosunLP mentioned this pull request Sep 19, 2026
feat(security): sanitize HTML without a DOM #238
Merged
8 tasks done
@JosunLP
Merge branch 'dev' into fix/219-exports-map
11633c9
@JosunLP
JosunLP marked this pull request as ready for review September 20, 2026 12:55
@claude
Merge branch 'fix/219-exports-map' into feat/218-publint-attw
c994887
The base branch picked up `dev`, which added `check:theme-links` on
the same lines this branch extends. Keep both: the `postbuild-types`
step on `build:types`, `check:theme-links` in the `check` chain, and
this branch's publint/attw scripts. `check:publish` still passes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
JosunLP
JosunLP commented Sep 20, 2026
View reviewed changes

@JosunLP JosunLP left a comment

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review of #236 (publint + attw gate, declaration-extension rewrite).

I verified the change end to end locally against the built dist/: bun run check:publint exits 0 (one deliberate false-esm warning), bun run check:attw exits 0, bun test tests/postbuild-types.test.ts 10/10 pass, bun run test:types clean, eslint and prettier --check clean on the touched files. No extensionless relative specifiers remain in any dist/**/*.d.ts statement, the lockfile has both new dev deps, and there is no prepare/prepack script, so attw's internal npm pack inside prepublishOnly cannot recurse. The resolver's file-before-directory preference matches what moduleResolution: Bundler did in the source, and there are no foo.d.ts + foo/index.d.ts collisions in dist/. Good approach overall.

4 findings, all low severity — one confirmed latent bug in the rewriter, one silent-success gap, one gate/doc mismatch, one doc off-by-one.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread scripts/postbuild-types.mjs
const distDir = join(repoRoot, 'dist');

/** `from '…'`, `import('…')` and bare `import '…'`, single or double quoted. */
const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])(\.{1,2}\/[^'"]*)\2/g;

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewriter is comment-blind — it already edits JSDoc text.

SPECIFIER is run over the raw declaration source, so a relative specifier inside a comment is rewritten exactly like a real import.

This is not hypothetical; it happens in the current build. src/component/types.ts:264 has

 * Can be either a plain string or a {@link import('./css').ComponentStyles}

and the emitted dist/component/types.d.ts:200 now reads {@link import('./css.js').ComponentStyles}. (Harmless there — arguably an improvement, since @link import types do resolve.)

The failure mode is @example blocks that show consumer-facing code. A doc comment emitted into dist/ containing import { $ } from './core' becomes './core/index.js', because dist/core/index.d.ts exists — the published example then points at bQuery's internal dist layout, which is wrong for the reader and shows up in IDE hover docs. The repo already has plenty of @example blocks with relative specifiers (src/router/index.ts:25, src/router/router.ts:72, src/i18n/index.ts:55); today none of them happen to collide with a real dist/ path, so this is latent rather than broken.

Cheapest mitigation: only match at statement positions (e.g. require the match to start at the beginning of a line after optional whitespace, which excludes * ... JSDoc continuation lines), or strip/skip comment ranges before rewriting.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread scripts/postbuild-types.mjs
return { fileCount: files.length, filesChanged, specifiersChanged };
}

export async function main() {

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent success when zero declarations are found.

main() guards only that dist/ exists. If collectDeclarations() comes back empty, the script still prints the ✓ line and exits 0.

Concrete way to trip it: change build:types to emit into dist/types, or reorder build so a vite build with emptyOutDir runs after build:types. The script then prints ✓ Declaration imports: rewrote 0 specifier(s) across 0 of 0 .d.ts files., bun run build is green locally, and the published declarations silently go back to the extensionless imports this PR exists to fix. Only the separate package CI job would notice.

That also cuts against the rule this PR's own doc restates two sections below for check:full-bundle: "If a module barrel cannot be read or parsed, that is also treated as a failure — silently skipping is no longer allowed."

Suggested change
export async function main() {
export async function main() {
if (!existsSync(distDir)) {
console.error('✗ No dist/ directory — run the type build first.');
process.exit(1);
}
const { fileCount, filesChanged, specifiersChanged } = await fixDeclarationExtensions();
if (fileCount === 0) {
console.error('✗ No .d.ts files under dist/ — the type build emitted nothing to rewrite.');
process.exit(1);
}

Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread package.json Outdated
"check:doc-exports": "bun scripts/check-doc-exports.mjs",
"check:theme-links": "bun scripts/check-theme-links.mjs",
"check:publint": "publint",
"check:attw": "attw --pack . --profile esm-only --ignore-rules false-esm",

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--ignore-rules false-esm is package-wide, not root-only.

attw applies --ignore-rules to every entry point, but the new release doc presents it as an exemption for the root only ("false-esm on the root"). That gap will hide a real regression:

if someone later adds a require condition to a sub-path — say "./core": { "require": "./dist/core.umd.cjs" } — while types still resolves into the ESM .d.ts tree, attw reports FalseESM for @bquery/bquery/core, this flag drops it, check:publish exits 0, and the broken sub-path ships. The maintainer, reading the doc, believes only the root is exempt.

Either split the run (one --entrypoints . invocation with the ignore, one for the rest without it), or reword the false-esm row in docs/contributing/release-process.md to say the ignore is package-wide and that a sub-path FalseESM would be masked too.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread docs/contributing/release-process.md Outdated
workflow (#218):

- **[`publint`](https://publint.dev)** — checks the `exports` map and the
tarball's contents. The map has 25 entries and is hand-edited on every new

@JosunLP JosunLP Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-by-one: the map has 26 entries on this branch, not 25.

$ node -e "console.log(Object.keys(require('./package.json').exports).length)"
26

A hard-coded count in prose will drift again on the next sub-path; consider dropping the number, or fixing it to 26.


Generated by Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
claude added 4 commits September 20, 2026 18:37
@claude
fix(build): give the require condition CommonJS types, and close fo…
fba4de9
…ur doc/test gaps

Review findings on #219:

- **Blocking.** `require` resolved to real CommonJS but its *types* still
  resolved to the ESM `dist/index.d.ts`, so a `module: node16` CJS consumer
  failed with TS1479 — reproduced against a real consumer package. `types`
  now lives inside each condition branch, and `scripts/postbuild-cts.mjs`
  emits a `.d.cts` tree beside the ESM one during `build:types`.

  A single copied `.d.cts` is not enough: it pushes the same error onto its
  own relative imports (24 errors with `skipLibCheck: false`). The whole tree
  has to exist, with specifiers resolved file-before-directory to the `.cjs`
  twin — `./utils` is a file in one module and a directory in another, and
  appending `.cjs` blindly yields TS2307. Measured on a real consumer:
  24 errors before, 0 after, with `skipLibCheck` both on and off. The ESM
  consumer and `require()`'s 638 runtime exports are unchanged.

- Documented CJS workaround 1 used top-level `await`, which is a SyntaxError
  in a `.cjs` file. It is now inside an async function.
- Workaround 2 sat directly under the `require('@bquery/bquery/server')`
  failure, so it read as the fix for it — but the UMD bundle stubs Node
  built-ins, so `server`/`ssr` APIs throw there. Scoped explicitly.
- The README's "Module format" link pointed at `bquery.dev`; the site is
  `bquery.js.org`.
- The regression guard checked the strings in the map but never that the
  targets exist, so reverting `vite.umd.config.ts`'s `fileName` alone kept
  all five tests green while `require()` failed with MODULE_NOT_FOUND. It
  now resolves every target against `dist/`, and asserts `require` types are
  `.d.cts`. Both new assertions fail against the reverted filename.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@claude
build: drop the attw ignore and close three gaps in the declaration r…
f4360d3
…ewrite

Merges the updated base (#219), which now emits a `.d.cts` tree, and builds
on it. Review findings on #218:

- `--ignore-rules false-esm` was package-wide while the release doc presented
  it as a root-only exemption, so a FalseESM on a sub-path that later grew a
  `require` condition would have been masked. With the `.d.cts` tree in
  place the ignore is no longer needed at all: `attw --profile esm-only`
  reports `node16 (from CJS): 🟢 (CJS)` for the root and publint prints
  "All good!", so the flag is removed rather than re-scoped.
- The rewriter was comment-blind and already edited JSDoc: `{@link
  import('./css')}` shipped as `'./css.js'`. Harmless there, but an
  `@example` showing consumer code would be rewritten to bQuery's internal
  dist layout and land in IDE hover docs. Comment lines are now skipped —
  625 specifiers rewritten instead of 626, the difference being exactly that
  `{@link}`.
- `main()` printed success and exited 0 when no declarations were found, so
  an `outDir` change or a build reorder would silently ship unrewritten
  declarations with a green build. That is now a failure, matching the rule
  the same doc states for `check:full-bundle`.
- The doc said the map has 25 entries; it has 26. The count is dropped
  rather than corrected, since it drifts on every new sub-path.

`build:types` runs both post-build steps: `postbuild-types` makes the ESM
specifiers explicit, then `postbuild-cts` mirrors the tree. Verified against
real consumer packages — 0 errors from both a `module: node16` CJS and an ESM
consumer with `skipLibCheck: false`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@claude
Merge branch 'dev' into fix/219-exports-map
b447133
#231 landed on `dev` as a squash, so the `check` chain there gained
`check:workflow-branches` while this branch still carried the pre-squash
merge — git cannot tell the two are the same content, hence the conflict.
Resolved to keep both: `dev`'s full check chain and this branch's
`postbuild-cts` step on `build:types`.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@claude
Merge branch 'fix/219-exports-map' into feat/218-publint-attw
0e13401
The base picked up `dev`, which gained `check:workflow-branches` from
#231's squash merge. Resolved to keep every script: both post-build steps
on `build:types`, the full check chain, and this branch's publint/attw
gate. `check:publish` still exits 0 with publint reporting "All good!".

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
Base automatically changed from fix/219-exports-map to dev September 21, 2026 08:05
claude and others added 2 commits September 21, 2026 08:09
@claude
Merge origin/dev into feat/218-publint-attw
0ebaef2
dev now carries #234 as a squash, so the branch's merge of
fix/219-exports-map conflicts with it. Resolved by keeping both sides
everywhere they diverge:

- package.json: build:types runs postbuild-types.mjs and
  postbuild-cts.mjs; check:publint/attw/publish sit beside
  check:package and check:coverage; prepublishOnly runs both.
- test.yml: the quality, coverage and package jobs coexist.
- npm-publish.yml: check:package then check:publish.
- release-process.md: the pre-publish list names both.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
@JosunLP
Merge branch 'dev' into feat/218-publint-attw
9faeb62
@JosunLP
JosunLP merged commit 8e24663 into dev Sep 21, 2026
27 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@JosunLP
JosunLP deleted the feat/218-publint-attw branch September 21, 2026 08:23
This was referenced Sep 21, 2026
[Feature]: Validate the published package with publint and are-the-types-wrong #218
Closed
release: 1.17.0 #245
Merged
JosunLP added a commit that referenced this pull request Sep 21, 2026
@JosunLP @claude
build: validate the published package with publint and are-the-types-…
656e795
…wrong (#236)

Co-authored-by: Claude Opus 5 <[email protected]>
Closes #218
JosunLP pushed a commit that referenced this pull request Sep 24, 2026
@claude
fix(build): keep rewriting specifiers that wrap across lines
9e02000
Both postbuild rewriters skipped comment text by splitting the source into
lines and matching each one on its own. `SPECIFIER` matches `\s*`/`\s+`
between the keyword and the quote, so a wrapped specifier —

    export type X = import(
      './css'
    ).Css;

only matches against the whole text. Per line it matched nothing, so the
extension was never added and the `.d.ts`/`.d.cts` shipped an unresolvable
import with the build still green. A rewrite that fails silently is worse
than one that over-reaches.

The guard now records the offsets of comment lines up front and the match
runs over the whole source, checking each hit's index against those ranges.
Comment text is still skipped, wrapped specifiers are rewritten again.

No multiline specifier is in the current `dist/` output, so nothing shipped
broken — but `postbuild-types.mjs` has carried this since #236 and
`prettier`'s print width decides when it stops being latent.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz
JosunLP added a commit that referenced this pull request Sep 24, 2026
@JosunLP @claude
release: 1.17.0 (#245)
71a7d16
## Summary

- Release PR for **1.17.0**, cutting everything on `dev` since `1.16.1`
into `main`.
- Feature release for `server` and `security`, plus the packaging fixes
that make the published tarball resolve correctly for the first time.
- **One user-visible break:** `dist/full.umd.js` no longer exists.
Details under [Upgrading](#upgrading).

> **You asked for 1.7.0.** `v1.7.0` is already tagged and the package is
on `1.16.1`, so that number would be a downgrade. Cut as **1.17.0** per
your confirmation.

## Motivation

`dev` is 14 commits ahead of `main` and carries four new features, one
lifecycle change, one reactive fix and the packaging work. Closes the
release cycle for #210, #211, #212, #213, #215, #216, #218, #219, #220,
#222, #223 and #229 — all twelve issues are closed and their PRs merged.

## What this PR's diff actually contains

> **Correction, kept in place.** This section originally read "No source
changes. The diff is 8 files plus the new release-notes page." That was
true of the release commit alone and has not been true of the PR for
some time. Left standing rather than quietly deleted, because the same
staleness is what the note below guards against.

The PR targets `main`, so GitHub shows the **whole `1.16.1 → 1.17.0`
delta: 73 files, +9985/−555** — all of `serveStatic`, `rateLimit`, both
sanitizer backends, the reactive scheduler and the packaging work.
Review it as a feature release, not as a version bump.

Measured against `dev`, the delta is **35 files, +1766/−258**: the
version bump, the release notes, and eleven source fixes pushed onto
this branch during review that are **not on `dev`**:

| Commit | Change |
| --- | --- |
| `1c6b5fb` | SSR validates tag and attribute names at every
serialization site — **this is the CodeQL `js/xss-through-dom` fix** |
| `f0c64b5` | `rateLimit` keys on the rightmost (proxy-added)
`X-Forwarded-For` hop |
| `407a6c5` | `trustProxy` reads one proxy-controlled header instead of
whichever turns up |
| `9e02000` | `postbuild-cts`/`postbuild-types` keep rewriting
specifiers that wrap across lines |
| `e668d14` | One text-extraction rule across both sanitizer backends;
the HTML-sink path escapes |
| `29dbcbf` | `serveStatic` can revalidate a precompressed asset, and
ranks encodings by q-value |
| `1192a87` | Limiters sharing a `SessionStore` serialize against each
other |
| `786bd4d` | A trailing slash no longer closes an element HTML will not
let close itself; entities decode on the DOM text-only path |
| `4d10b5e` | Character references decode by the tokenizer's rules
(case, `;`, legacy prefixes, attribute rule) from an HTML 4.01 + ASCII +
legacy table; **raises five bundle budgets by ~1.8 kB** |
| `4c269a4` | A precompressed sidecar's `ETag`/`Last-Modified` come from
the sidecar; its `304` no longer offers ranges |
| `0fc9849` | The workflow branch guard fails on unreadable triggers
even when it has no branch list |

## Version and metadata

- `package.json`: `1.16.1` → `1.17.0`.
- `AGENT.md`, `llms.txt`, `.cursorrules`, `.clinerules`,
`.github/copilot-instructions.md`: version baseline and a new highlights
section each. `bun run check:ai-guidance` enforces these and passes.

**Changelog**

- `## [Unreleased]` → `## [1.17.0] - 2026-09-21`, section headings and
the table of contents renamed to match.
- **Three Changed entries added that were missing**, plus a Fixed entry
per review-round fix. See the note below.

**Docs**

- New `docs/release-notes/1.17.md` — the release process requires a page
per minor.
- Row added to `docs/release-notes/index.md`.

## The changelog gap I had to close first

The `Unreleased` block on `dev` covered only #205, #210, #222, #223 and
#229. The packaging work had merged with **no changelog entry at all**:

| Merged | Missing from the changelog |
| --- | --- |
| #219 (PR #234) | `dist/full.umd.js` → `.cjs`, CDN fields →
`full.iife.js`, `require()` fixed, `types` first, `./package.json`
export, `.d.cts` tree |
| #218 (PR #236) | Extensionless declaration imports fixed (315 ×
`TS2834` → 0) |
| #220 (PR #235) | JS source maps dropped from the tarball |

The first row is this release's only breaking change. A release that
ships a 404 for pinned CDN URLs without a changelog line is how users
find out the hard way, so those three entries are now written up under
`### Changed (1.17.0)`.

The remaining merged work (#211, #212, #213, #215, #216, #243, #244) is
CI and repo tooling with no effect on consumers, so it stays out of the
changelog by design.

## Validation

Against `0fc9849`:

- [x] `bun run lint` — clean
- [x] `bun run build`
- [x] `bun test` — **3518 pass, 1 skip, 0 fail**
- [x] `npx tsc --noEmit` — clean

| Gate | Result |
| --- | --- |
| `check:ai-guidance` | in sync at 1.17.0 |
| `check` (full-bundle, stability, doc-exports, theme-links,
workflow-branches) | pass |
| `format:check` | pass |
| `check:package` | 1.38 MB packed / 5.39 MB unpacked / 1240 files |
| `check:size` | 25/25 within budget, largest `./full` at 133.4 kB
gzipped |
| `check:publish` | publint + attw exit 0 |
| `check:coverage` | 89.27% lines / 87.88% functions |
| `build:docs` | VitePress builds with the new release-notes page |

The entity decoder was also checked outside the test suite:
differential-tested against Chromium's `DOMParser` on 60,046 generated
text and attribute inputs, with 0 differences outside names the table
deliberately leaves out (the previous decoder differed on 9,240).

**One unexplained result, recorded rather than rounded off.** An earlier
full-suite run reported `3497 pass / 1 fail`. It has not recurred in 23
further full-suite runs or 25 targeted runs of the changed files, every
test file passes in isolation, and the output gave only the counts — so
I do not have the name of the test that failed. Calling it a flake would
be a guess; it is listed here because an unreproduced failure is worth a
reviewer knowing about.

## Checklist

- [x] I followed the contributing guidelines.
- [x] I added or updated tests where behavior changed.
- [x] I updated documentation when public behavior or developer workflow
changed.
- [x] I confirmed the change does not introduce security-sensitive
regressions.

## Upgrading

Package specifiers (`@bquery/bquery`, `@bquery/bquery/*`) are unaffected
and resolve better than before. One path changed:

| Before | After |
| --- | --- |
| `dist/full.umd.js` | `dist/full.umd.cjs` |
| `unpkg` / `jsdelivr` → UMD build | → `dist/full.iife.js` |

A pinned `unpkg.com/@bquery/bquery@1/dist/full.umd.js` will **404**.
`full.iife.js` sets the same `window.bQuery` global.

Behaviour changes worth checking:

- `app.use()` middleware now runs for requests matching no route.
- `batch()` runs effects only after derivations settle, so code relying
on observing an intermediate state inside a batch sees the settled
state.
- SSR drops attribute and tag names outside the validated ASCII
patterns, so a non-ASCII name such as `<div é="x">` is no longer
serialized.
- **In the browser**, `stripTags()` no longer surfaces
`<script>`/`<style>` source as text —
`stripTags('<script>alert(1)</script>')` is now `''` rather than
`'alert(1)'`, matching the server.
- **In the browser**, `sanitizeHtml(html, { stripAllTags: true })` now
escapes its output. It is branded `SanitizedHtml` and callers assign it
to HTML sinks, so returning entity-decoded text raw handed back live
markup. `stripTags()` is unaffected and still returns raw plain text.
- The sanitizer decodes character references like a browser:
`caf&eacute;` is `café` on the server and on the browser's text-only
path, names are case-sensitive, and `href="?a=1&copy=2"` is left intact.
HTML5-only names outside the table (e.g. `&star;`) stay literal on the
string backend. Every entry that sanitizes (`core`, `component`,
`security`, `view`, `storybook`, and the umbrella entries) grows by
about **1.8 kB gzipped**.

## Notes for reviewers

**Why 1.17.0 and not 2.0.0.** There is a defensible major case, and it
got slightly stronger during review: the UMD path removal, the
`app.use()` lifecycle change, the `batch()` ordering change and now two
browser-visible sanitizer changes are all observable. I still read them
as minor because no exported API was removed or changed shape, the UMD
break is a deep file path rather than a package specifier, the sanitizer
changes remove a leak rather than alter a contract, and #242
deliberately kept `'sync'` the default scheduler for all of 1.x
precisely to avoid forcing a major. If you weigh the CDN path break
higher, say so and I will recut as 2.0.0 — it is a re-run of the same
mechanical edits.

**The entity table is a size trade-off you may want to overrule.** It is
every HTML 4.01 name plus every ASCII-valued and legacy-prefixed name:
~1.3 kB of the ~1.8 kB. The full HTML5 table would be ~7.4 kB; dropping
the Greek and math symbols would save ~0.6 kB at the cost of `&alpha;`,
`&le;`, `&infin;` and friends diverging between backends. Either
direction is a small follow-up.

**Branch choice, and why the back-merge is mandatory.** This is
`release/1.17.0` off `dev` rather than `dev → main` directly, so `dev`
stays at `1.16.1` if you defer the release. That was the only cost when
this PR opened. It is not any more: the eleven fixes listed above live
**only on this branch**. Merging to `main` without back-merging `main`
into `dev` would leave `dev` without the CodeQL fix, both rate-limiter
hardening commits, the shared-store lock and the sanitizer fixes — and
the next release cut from `dev` would reintroduce all of them as
regressions. So after merge, either back-merge `main` into `dev`, or say
the word and I will redo this as a straight `dev → main` PR, which
removes the follow-up entirely.

**`main` has one commit `dev` does not** (`fe51cb5`, the squash of
#208). The merge is conflict-free — verified with `git merge-tree` — but
that squash is why `dev` and `main` diverge rather than fast-forward.
Note also that `dev`'s squash-merges carry different SHAs than the same
content on this branch, so compare the two by content (`git diff
origin/dev origin/release/1.17.0`), not by commit list.

**Not yet done, deliberately:** no git tag and no `npm publish`. Tagging
`v1.17.0` belongs on the merge commit, and publishing is maintainer-only
per the release process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Y2dPuUeGeK7mJvar9uLqKz)_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added static file serving with caching, range requests, compressed
assets, directory indexes, and traversal protection.
* Added fixed-window rate limiting with standard headers and `429`
responses; a key function is required unless trusted proxy headers are
used.
  * Added DOM-free HTML sanitization with configurable backends.
* Added opt-in batched reactive scheduling, scheduler configuration, and
synchronous flushing.
  * Global middleware now runs for unmatched routes.

* **Changed**
* Renamed the CommonJS bundle from `full.umd.js` to `full.umd.cjs`;
browser scripts use `full.iife.js`.
  * Improved package type declarations and source-map handling.
  * Reactive updates now settle derived values before effects run.

* **Bug Fixes**
* Hardened server-rendered HTML by rejecting invalid tag and attribute
names.
* Improved compressed-asset revalidation and sanitizer handling of HTML
entities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

build Changes to the build and meta files ci/cd Changes to the action workflows docs Changes to the documentation tests Chenges to the tests

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@JosunLP @claude
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.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.