Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package-lock=false
allow-same-version=true
message=v%s
allow-same-version=false
message=v%s
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Generated from conventional commit messages. Breaking changes are collected
under their own heading, from either a `!` after the type or a
`BREAKING CHANGE:` footer.

Releases up to 1.3.0 are kept as they were published, in
Releases up to 1.3.8 are kept as they were published, in
tools/changelog-history.md.

## [1.3.8](https://github.com/GSTJ/safe-jsx/compare/v1.3.7...v1.3.8) (2026-08-05)
Expand Down
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,25 @@ at all.
3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of other developer, or if you do not have permission to do that, you may request the reviewer to merge it for you.

## Releases

Run `npm version patch`, `npm version minor` or `npm version major` on a release
branch. npm updates the package, rebuilds the changelog, creates the release
commit and prepares an annotated tag from the same release notes.

Push the branch without `--tags` and merge its PR. From the updated `main`
branch, run:

```sh
pnpm run release:tag
RELEASE_VERSION=$(node -p 'require("./package.json").version')
git push origin "v${RELEASE_VERSION}"
```

`release:tag` can move the local tag from the release branch to its squash
commit while the tag is unpublished. It refuses to change a tag that already
exists on `origin`. Create the GitHub release from that tag and use the output
from `pnpm run release-notes` as its body; the release event publishes the npm
package with provenance.

Thank you for considering contributing to @gstj/safe-jsx!
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@
"format": "oxfmt --check .",
"format:fix": "oxfmt .",
"typecheck": "tsc --noEmit",
"test": "jest --coverage",
"test": "jest --coverage && node tools/release-check.mjs",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prepublishOnly": "safe-publish-latest && npm run lint && npm run test",
"changelog": "node tools/changelog.mjs",
"changelog:check": "node tools/changelog-check.mjs",
"release-notes": "node tools/release-notes.mjs",
"release:check": "node tools/release-check.mjs",
"release:tag": "node tools/tag-release.mjs",
"version": "npm run changelog && git add CHANGELOG.md",
"postversion": "node tools/release-notes.mjs > .release-notes && git tag -f -a --cleanup=verbatim \"v$npm_package_version\" -F .release-notes && rm -f .release-notes"
"postversion": "npm run release:tag"
},
"devDependencies": {
"@types/estree": "^1.0.9",
Expand Down
56 changes: 38 additions & 18 deletions tools/changelog-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,17 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { env } from "node:process";
import { pathToFileURL } from "node:url";

import { ConventionalChangelog } from "conventional-changelog";
import { Bumper } from "conventional-recommended-bump";

import preset, { TYPES } from "./changelog-preset.mjs";
import preset, { formatCommitDate, TYPES } from "./changelog-preset.mjs";

const here = import.meta.dirname;
const presetPath = join(here, "changelog-preset.mjs");
// Resolved rather than assumed to sit at ../node_modules/.bin: pnpm hoists, and
// in a workspace the binary can land in the root store instead of the package.
// The package exports no `./package.json`, so resolve its entry point and walk
// across to the CLI beside it.
const cliEntry = fileURLToPath(import.meta.resolve("conventional-changelog"));
const cliPath = join(dirname(cliEntry), "cli", "index.js");

/**
* What each type is for. `bump` can raise the version, `changelog` renders
Expand Down Expand Up @@ -122,7 +118,16 @@ const inThrowawayRepo = async (commits, tag, run) => {
const repo = mkdtempSync(join(tmpdir(), "safe-jsx-changelog-"));
/** @param {string[]} args */
const git = (...args) =>
execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: "pipe" });
execFileSync("git", args, {
cwd: repo,
encoding: "utf8",
env: {
...env,
GIT_AUTHOR_DATE: "2026-08-05T21:03:25-03:00",
GIT_COMMITTER_DATE: "2026-08-05T21:03:25-03:00",
},
stdio: "pipe",
});

try {
git("init", "--quiet", "--initial-branch", "main");
Expand Down Expand Up @@ -162,13 +167,19 @@ const inThrowawayRepo = async (commits, tag, run) => {
* @returns {Promise<string>}
*/
const render = (configPath) =>
inThrowawayRepo(COMMITS, "after", (repo) =>
execFileSync(
process.execPath,
[cliPath, "--config", configPath, "--release-count", "0", "--stdout"],
{ cwd: repo, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
),
);
inThrowawayRepo(COMMITS, "after", async (repo) => {
const config =
/** @type {{ default: Parameters<ConventionalChangelog["config"]>[0] }} */ (
await import(pathToFileURL(configPath).href)
);
const generator = new ConventionalChangelog(repo)
.readPackage()
.config(config.default)
.options({ formatDate: formatCommitDate, releaseCount: 0 });
let output = "";
for await (const chunk of generator.write()) output += chunk;
return output;
});

/**
* The release type conventional-recommended-bump lands on for `commits`, read
Expand Down Expand Up @@ -214,6 +225,10 @@ for (const [type, effect] of Object.entries(POLICY)) {
for (const entry of TYPES) {
expect(`${entry.type} is covered by the policy`, entry.type in POLICY);
}
expect(
"release dates keep the calendar day recorded by git",
formatCommitDate("2026-08-05 21:03:25 -0300") === "2026-08-05",
);

/**
* 2. The rendered output says what the policy says.
Expand Down Expand Up @@ -250,7 +265,12 @@ const assessRendering = (output) => {
return found;
};

failures.push(...assessRendering(await render(presetPath)));
const rendered = await render(presetPath);
failures.push(...assessRendering(rendered));
expect(
"rendered release dates keep the calendar day recorded by git",
rendered.includes(" (2026-08-05)"),
);

// 3. `effect` decides the bump, not just the rendering. The middle case is the
// one that matters: those four types all render, and none of them may push a
Expand Down
110 changes: 110 additions & 0 deletions tools/changelog-history.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,113 @@
## [1.3.8](https://github.com/GSTJ/safe-jsx/compare/v1.3.7...v1.3.8) (2026-08-05)

### Bug Fixes

* **package:** correct the Node engine floor ([#72](https://github.com/GSTJ/safe-jsx/issues/72)) ([fe09789](https://github.com/GSTJ/safe-jsx/commit/fe0978959e9b3f1b92bb66dae5fe7fb28ab3fd99))

### Chores

* **deps:** lock file maintenance ([#70](https://github.com/GSTJ/safe-jsx/issues/70)) ([59d86c3](https://github.com/GSTJ/safe-jsx/commit/59d86c32e328879be750750ac02035d76be03bda))
* **deps:** quarantine new releases for 14 days ([#71](https://github.com/GSTJ/safe-jsx/issues/71)) ([93dfff1](https://github.com/GSTJ/safe-jsx/commit/93dfff1a3191815877089beb7e838fac1b3a5cdb))
* **deps:** update dependency @typescript-eslint/parser to v8.66.0 ([#67](https://github.com/GSTJ/safe-jsx/issues/67)) ([e58969d](https://github.com/GSTJ/safe-jsx/commit/e58969d2567103aa91a38363521606e494dd5605))
* **deps:** update github actions ([#65](https://github.com/GSTJ/safe-jsx/issues/65)) ([d5fe3b1](https://github.com/GSTJ/safe-jsx/commit/d5fe3b1504fad966ffdf2d8b4cb7fde8c11f0068))
* **deps:** update magic tooling ([#66](https://github.com/GSTJ/safe-jsx/issues/66)) ([35be0bd](https://github.com/GSTJ/safe-jsx/commit/35be0bd931c3e437aad1110fc7836c8cb42d63a7))
* **deps:** update oxc toolchain ([#68](https://github.com/GSTJ/safe-jsx/issues/68)) ([c61ec9e](https://github.com/GSTJ/safe-jsx/commit/c61ec9e0e6f8b04c0055057c4378e5c3ec284449))
* **deps:** update pnpm to v11.20.0 ([#69](https://github.com/GSTJ/safe-jsx/issues/69)) ([3bb5ce0](https://github.com/GSTJ/safe-jsx/commit/3bb5ce09f197d36c34924342b7580f0da967eb47))

## [1.3.7](https://github.com/GSTJ/safe-jsx/compare/v1.3.6...v1.3.7) (2026-08-04)

### Bug Fixes

* **jsx-explicit-boolean:** bound boolean evidence traversal ([#61](https://github.com/GSTJ/safe-jsx/issues/61)) ([4fc3c98](https://github.com/GSTJ/safe-jsx/commit/4fc3c984e23837a36ffd6e48e017808d60c3d124))

### Chores

* **deps:** lock file maintenance ([#58](https://github.com/GSTJ/safe-jsx/issues/58)) ([1c557ca](https://github.com/GSTJ/safe-jsx/commit/1c557cae041e540a9113cb39cd6595dbf324e1ae))
* **deps:** patch brace-expansion ([#54](https://github.com/GSTJ/safe-jsx/issues/54)) ([523cb0b](https://github.com/GSTJ/safe-jsx/commit/523cb0b9694092a407845977fc6c0db12e7b7629))
* **deps:** update dependency @types/node to v26.1.2 ([#50](https://github.com/GSTJ/safe-jsx/issues/50)) ([51d1658](https://github.com/GSTJ/safe-jsx/commit/51d165866e32d95dd4ee54877181a845fb757adc))
* **deps:** update dependency magic-oxlint-config to v2 ([#52](https://github.com/GSTJ/safe-jsx/issues/52)) ([e267289](https://github.com/GSTJ/safe-jsx/commit/e26728997b64a47d17ed059c5e9cfb6e975404fe))
* **deps:** update oxc toolchain ([#51](https://github.com/GSTJ/safe-jsx/issues/51)) ([c663647](https://github.com/GSTJ/safe-jsx/commit/c663647626e6f97c7c61f6245120ac5190a6fd1a))
* **deps:** update pnpm to v11.18.0 ([#53](https://github.com/GSTJ/safe-jsx/issues/53)) ([af4d497](https://github.com/GSTJ/safe-jsx/commit/af4d4979bcb37e43b21a19f1cbde95d245c021f7))
* **deps:** update pnpm to v11.19.0 ([#59](https://github.com/GSTJ/safe-jsx/issues/59)) ([6e0b5e1](https://github.com/GSTJ/safe-jsx/commit/6e0b5e1dfe18aefdccf62c615a6546f7dbc374af))
* **deps:** update pnpm/action-setup action to v6 ([#62](https://github.com/GSTJ/safe-jsx/issues/62)) ([2780bc7](https://github.com/GSTJ/safe-jsx/commit/2780bc7ee3c735b8a4bdb40dad8a2128a14ef4fe))
* **renovate:** keep overrides within major ([#57](https://github.com/GSTJ/safe-jsx/issues/57)) ([a7620f7](https://github.com/GSTJ/safe-jsx/commit/a7620f7a1229ef96abbf36819ab63133608db275))

## [1.3.6](https://github.com/GSTJ/safe-jsx/compare/v1.3.5...v1.3.6) (2026-07-28)

### Bug Fixes

* **jsx-explicit-boolean:** stop trusting a shadowed Boolean ([#49](https://github.com/GSTJ/safe-jsx/issues/49)) ([d140eb5](https://github.com/GSTJ/safe-jsx/commit/d140eb5de0f02c99aa766c6713090c9d57bbde92))

## [1.3.5](https://github.com/GSTJ/safe-jsx/compare/v1.3.4...v1.3.5) (2026-07-28)

### Build System

* rewrite the security policy and ship it in the tarball ([#47](https://github.com/GSTJ/safe-jsx/issues/47)) ([78bc88f](https://github.com/GSTJ/safe-jsx/commit/78bc88f7791cda50b63fd6e2ce84aaa13accb8ff))

### Chores

* drop the npmignore leftovers from [#44](https://github.com/GSTJ/safe-jsx/issues/44) ([#46](https://github.com/GSTJ/safe-jsx/issues/46)) ([00cc4d9](https://github.com/GSTJ/safe-jsx/commit/00cc4d904ab8a900221a1d2e16b91439bcc47df1))

## [1.3.4](https://github.com/GSTJ/safe-jsx/compare/v1.3.3...v1.3.4) (2026-07-28)

### Build System

* keep CHANGELOG.md in the published tarball ([#45](https://github.com/GSTJ/safe-jsx/issues/45)) ([487f5ef](https://github.com/GSTJ/safe-jsx/commit/487f5ef40ddec413d22a647930a6c95bf6e41439)), references [#44](https://github.com/GSTJ/safe-jsx/issues/44)

## [1.3.3](https://github.com/GSTJ/safe-jsx/compare/v1.3.2...v1.3.3) (2026-07-28)

### Build System

* make the eslint peer optional ([b0c36cb](https://github.com/GSTJ/safe-jsx/commit/b0c36cba0c2b47cc1b529aae2ac50e95f0961a69))
* publish with npm's files field instead of npmignore ([b49634e](https://github.com/GSTJ/safe-jsx/commit/b49634e4848350548ad3847e77462d68eb8303fd))

## [1.3.2](https://github.com/GSTJ/safe-jsx/compare/v1.3.1...v1.3.2) (2026-07-28)

### Bug Fixes

* **deps:** make the eslint peer optional ([0f89f8a](https://github.com/GSTJ/safe-jsx/commit/0f89f8a986749f8c3e46a95b62f2559dc1b849ce))

### Chores

* **changelog:** render the types that can change the tarball ([8303da9](https://github.com/GSTJ/safe-jsx/commit/8303da983839255670aa76a74d5eecff846db00f))

### Documentation

* **contributing:** say which commit types reach the changelog ([32ca7f9](https://github.com/GSTJ/safe-jsx/commit/32ca7f99e0feeb83fa803d75d9e78ff518970862))

## [1.3.1](https://github.com/GSTJ/safe-jsx/compare/v1.3.0...v1.3.1) (2026-07-28)

### Bug Fixes

* **release:** stop running oxfmt over the generated CHANGELOG ([0abd1fe](https://github.com/GSTJ/safe-jsx/commit/0abd1fe55f6e883e09a8474dc5eb99d37e086c84))

### Build System

* drop the dev tsconfig from the tarball ([4c41336](https://github.com/GSTJ/safe-jsx/commit/4c413366e289183aca0cb525071aac656f7eeb18))
* drop the incremental workaround ([b47fc38](https://github.com/GSTJ/safe-jsx/commit/b47fc38afe36b2e59d2ba16b636c127a5863e990))
* keep dev config out of the tarball and fix the incremental build ([4c4b2a0](https://github.com/GSTJ/safe-jsx/commit/4c4b2a0c543e6b6b99f4266da717ee25dcefa6f3))

### Code Refactoring

* **rules:** make LegacyRuleContext a type alias ([4255fe3](https://github.com/GSTJ/safe-jsx/commit/4255fe3b951a81e66b83fa18c4d61bd9c7cb2e75))
* type the plugin sources against the strict shared tsconfig ([0bc750f](https://github.com/GSTJ/safe-jsx/commit/0bc750f73b521dda2e4eb644a3e0523fe6a3aba2))

### Chores

* **changelog:** rebuild in the generator's own bullet style ([0f15905](https://github.com/GSTJ/safe-jsx/commit/0f15905c891d68ce524646877263b4a904db7c5e))
* **config:** adopt the magic oxlint, oxfmt and tsconfig presets ([9423539](https://github.com/GSTJ/safe-jsx/commit/9423539818b9d53d20f82e6e86958997fc4378b7))
* **config:** build the lint config with extendConfig ([df8f17f](https://github.com/GSTJ/safe-jsx/commit/df8f17f644787744fd714018a5de786b08cbc92e))
* **deps:** move the magic stack to 1.1.0 ([55f8751](https://github.com/GSTJ/safe-jsx/commit/55f87518849789dde154fc55c2b119e144399a8e))
* **deps:** move the magic stack to 1.2.0 ([008b298](https://github.com/GSTJ/safe-jsx/commit/008b298024171fae03e1ef2f6da38ed2abd11f6d))
* **deps:** move to pnpm and swap eslint/prettier tooling for the magic stack ([faceee1](https://github.com/GSTJ/safe-jsx/commit/faceee1310e32e5f4bf91ec3b6122c4e3db0adee))
* **deps:** replace the deprecated conventional-changelog-cli ([27f1e1e](https://github.com/GSTJ/safe-jsx/commit/27f1e1edb5ccaf1f6436e680ea37044eb56754fa)), references [#33](https://github.com/GSTJ/safe-jsx/issues/33)
* **renovate:** say what actually sets the typescript ceiling ([11a0f20](https://github.com/GSTJ/safe-jsx/commit/11a0f20f90cc1b4706e3542b99d82c73120dd9c6))

### Documentation

* **config:** correct why this config uses extendConfig ([2fc12b3](https://github.com/GSTJ/safe-jsx/commit/2fc12b383bc9d92ac3c4169dc42b20d5c2fa5410))
* **config:** say why the eslint-plugin meta rules are not wired up ([9cef61c](https://github.com/GSTJ/safe-jsx/commit/9cef61c8d6c185e555362a94b97872f9c68966fe))

## [1.3.0](https://github.com/GSTJ/safe-jsx/compare/v1.2.0...v1.3.0) (2026-07-27)

### Bug Fixes
Expand Down
21 changes: 20 additions & 1 deletion tools/changelog-preset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
// positive control for that.
import createPreset from "conventional-changelog-conventionalcommits";

/**
* Keeps a tagged release on the calendar date recorded by git. The writer's
* default converts offset-aware timestamps to UTC, which can move a late
* release into the next day when the changelog is regenerated.
*
* @param {string | Date} date
* @returns {string}
*/
export const formatCommitDate = (date) =>
typeof date === "string"
? date.slice(0, 10)
: date.toISOString().slice(0, 10);

/** @type {import("conventional-changelog-conventionalcommits").CommitType[]} */
export const TYPES = [
{ type: "feat", section: "Features", effect: "bump" },
Expand All @@ -46,4 +59,10 @@ export const TYPES = [
{ type: "test", section: "Tests", effect: "hidden" },
];

export default createPreset({ types: TYPES });
const preset =
/** @type {{ writer: { formatDate?: typeof formatCommitDate } }} */ (
/** @type {unknown} */ (createPreset({ types: TYPES }))
);
preset.writer.formatDate = formatCommitDate;

export default preset;
31 changes: 13 additions & 18 deletions tools/changelog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@
// 2023 — it also moves 1.0.1's compare link, since the first commit the range
// starts from changes once chore is visible. Published notes stay as published.
// Everything above the freeze is still generated from git on every run.
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

const FROZEN_AT = "1.3.0";
import { ConventionalChangelog } from "conventional-changelog";

import preset, { formatCommitDate } from "./changelog-preset.mjs";

const FROZEN_AT = "1.3.8";

const here = import.meta.dirname;
const historyPath = join(here, "changelog-history.md");
const presetPath = join(here, "changelog-preset.mjs");

const header = `# Changelog

Expand All @@ -33,20 +35,12 @@ tools/changelog-history.md.

`;

const body = execFileSync(
process.platform === "win32" ? "npx.cmd" : "npx",
[
"conventional-changelog",
"--config",
presetPath,
"--release-count",
"0",
// Without this the CLI writes CHANGELOG.md itself and leaves stdout empty,
// which would skip the header and trip the guard below.
"--stdout",
],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
);
const generator = new ConventionalChangelog()
.readPackage()
.config(preset)
.options({ formatDate: formatCommitDate, releaseCount: 0 });
let body = "";
for await (const chunk of generator.write()) body += chunk;

if (!body.trim()) {
console.error(
Expand All @@ -68,6 +62,7 @@ if (freezeIndex === -1) {

const generated = body.slice(0, freezeIndex).trim();
const history = readFileSync(historyPath, "utf8").trim();
const current = generated ? `${generated}\n\n${history}` : history;

writeFileSync("CHANGELOG.md", `${header + generated}\n\n${history}\n`);
writeFileSync("CHANGELOG.md", `${header + current}\n`);
console.log("CHANGELOG.md rebuilt");
Loading
Loading