Skip to content

Commit 671564e

Browse files
matmenclaude
andcommitted
publish to JSR alongside npm (#33, #38)
jsr.json publishes the TypeScript sources as @matmen/imagescript, its version is kept in sync by the build. The release workflow publishes to JSR after npm through GitHub OIDC, the release check makes sure the versions match and JSR doesn't have the version yet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
1 parent beba5a5 commit 671564e

7 files changed

Lines changed: 54 additions & 10 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
name: release
22

3-
# publishing a GitHub release with the tag v<version> publishes that version to npm:
4-
# the release is checked (tag, semver, changelog, not yet on npm), tested on every supported platform,
5-
# published with provenance through npm trusted publishing (no token), then the docs are deployed
3+
# publishing a GitHub release with the tag v<version> publishes that version to npm and JSR:
4+
# the release is checked (tag, semver, changelog, jsr.json, not yet on npm/JSR), tested on every supported platform,
5+
# published to npm with provenance through trusted publishing and to JSR through GitHub OIDC (no tokens),
6+
# then the docs are deployed
67
on:
78
release:
89
types: [published]
@@ -57,6 +58,23 @@ jobs:
5758
env:
5859
DIST_TAG: ${{ needs.check.outputs.dist-tag }}
5960

61+
jsr:
62+
needs: publish
63+
runs-on: ubuntu-latest
64+
environment:
65+
name: jsr
66+
url: https://jsr.io/@matmen/imagescript/versions
67+
permissions:
68+
contents: read
69+
id-token: write
70+
steps:
71+
- uses: actions/checkout@v7
72+
- uses: denoland/setup-deno@v2
73+
with:
74+
deno-version: v2.x
75+
# publishes the TypeScript sources as they are, JSR authenticates the workflow through the linked repository
76+
- run: deno publish
77+
6078
docs:
6179
needs: [check, publish]
6280
if: needs.check.outputs.dist-tag == 'latest'

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ ImageScript is now written in TypeScript and has no WebAssembly or native binari
66
was rewritten, the npm package shrank from 3 MB to about 120 kB, and it installs and runs on any platform Node.js
77
supports (including Alpine/musl and bundled serverless functions).
88

9+
On JSR, `@matmen/imagescript` is now the same package as on npm (it used to be the Deno-only 1.x with WebAssembly),
10+
published from the TypeScript sources with every release: `deno add jsr:@matmen/imagescript`.
11+
912
### Breaking changes
1013

1114
- **Node.js 22.13 or newer is required.** The package is ES modules only; `require('imagescript')` keeps working

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99

1010
**ImageScript** is a zero-dependency alternative to common JavaScript bitmap image manipulation tools. Every codec and
1111
renderer is written in TypeScript: no WebAssembly, no native binaries, no npm dependencies, no install scripts. The
12-
package is about 120 kB (compressed) and runs anywhere Node.js 22.13 or newer runs, including Alpine/musl, ARM and bundled
13-
serverless functions. It works with both `import` and `require()`, type definitions are included.
12+
package is about 120 kB (compressed) and runs in Node.js 22.13 or newer (including Alpine/musl, ARM and bundled
13+
serverless functions), Deno, Bun and browsers. It works with both `import` and `require()`, type definitions are included.
1414

1515
```sh
16-
npm install imagescript
16+
npm install imagescript # Node.js, Bun and bundlers
17+
deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
1718
```
1819

1920
---

‎jsr.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@matmen/imagescript",
3+
"version": "3.0.0",
4+
"license": "(AGPL-3.0-or-later OR MIT)",
5+
"exports": "./src/index.ts",
6+
"publish": {
7+
"include": ["src", "README.md", "LICENSE", "LICENSE.MIT", "LICENSE.AGPL-3.0"]
8+
}
9+
}

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"docs": "node scripts/docs.js",
2626
"test": "npm run build && node ./tests/run.js",
2727
"prepack": "npm run build",
28-
"version": "node scripts/build.js && git add src/version.ts"
28+
"version": "node scripts/build.js && git add src/version.ts jsr.json"
2929
},
3030
"repository": {
3131
"type": "git",

‎scripts/build.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ const run = args => execFileSync(process.execPath, [tsc, ...args], { cwd: root,
1111
// keep the version reported in PNG metadata in sync with package.json
1212
const { version } = require('../package.json');
1313
fs.writeFileSync(path.join(root, 'src/version.ts'), `// kept in sync with package.json by scripts/build.js\nexport const VERSION = '${version}';\n`);
14+
// and the one JSR publishes
15+
const jsrPath = path.join(root, 'jsr.json');
16+
const jsr = JSON.parse(fs.readFileSync(jsrPath, 'utf8'));
17+
if (jsr.version !== version) fs.writeFileSync(jsrPath, JSON.stringify({ ...jsr, version }, null, 2) + '\n');
1418

1519
fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true });
1620
// declarations keep the doc comments (editors show them), the JavaScript doesn't need them

‎scripts/release-check.js‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
// checks a GitHub release before it is published to npm, run by .github/workflows/release.yml:
2-
// the tag matches package.json, the version is valid semver, newer than what npm has and listed in the changelog,
3-
// prereleases are marked as such. Prints the npm dist-tag to publish under (and writes it to GITHUB_OUTPUT)
1+
// checks a GitHub release before it is published to npm and JSR, run by .github/workflows/release.yml:
2+
// the tag matches package.json and jsr.json, the version is valid semver, newer than what npm has, not on JSR yet and
3+
// listed in the changelog, prereleases are marked as such. Prints the npm dist-tag to publish under (and writes it to GITHUB_OUTPUT)
44
// usage: node scripts/release-check.js <tag> [prerelease flag of the GitHub release: true/false]
55
const fs = require('fs');
66
const path = require('path');
77

88
const root = path.join(__dirname, '..');
99
const { name, version } = require('../package.json');
10+
const jsr = require('../jsr.json');
1011
const [tag = '', prereleaseFlag = 'false'] = process.argv.slice(2);
1112

1213
const errors = [];
@@ -50,6 +51,8 @@ async function main() {
5051
fail(current.pre.length ? `${version} is a prerelease version, mark the GitHub release as prerelease`
5152
: `the GitHub release is marked as prerelease, but ${version} has no prerelease suffix (like -beta.1)`);
5253

54+
if (jsr.version !== version) fail(`jsr.json has version ${jsr.version}, package.json ${version} (npm run build syncs them)`);
55+
5356
const changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8');
5457
if (!changelog.split('\n').some(line => /^##\s/.test(line) && line.replace(/^##\s+v?/, '').trim().split(/\s/)[0] === version))
5558
fail(`CHANGELOG.md has no "## ${version}" section`);
@@ -83,6 +86,12 @@ async function main() {
8386
else if (latest && compare(current, latest) < 0) distTag = `v${current.major}-latest`;
8487
}
8588

89+
// JSR has no dist-tags, its "latest" is the newest stable version
90+
const [scope, jsrName] = jsr.name.slice(1).split('/');
91+
const jsrRes = await fetch(`https://api.jsr.io/scopes/${scope}/packages/${jsrName}/versions/${encodeURIComponent(version)}`);
92+
if (jsrRes.ok) fail(`${jsr.name}@${version} is already published on JSR`);
93+
else if (jsrRes.status !== 404) throw new Error(`JSR answered ${jsrRes.status}`);
94+
8695
if (errors.length) {
8796
for (const e of errors) console.error(`::error::${e}`);
8897
process.exit(1);

0 commit comments

Comments
 (0)