<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: fabon</title>
    <description>The latest articles on DEV Community by fabon (@fabon).</description>
    <link>https://dev.to/fabon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1444164%2F1e498afa-5584-4515-bad0-15b152b478d3.png</url>
      <title>DEV Community: fabon</title>
      <link>https://dev.to/fabon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fabon"/>
    <language>en</language>
    <item>
      <title>Publish pure ESM npm package written in TypeScript to JSR</title>
      <dc:creator>fabon</dc:creator>
      <pubDate>Sun, 12 May 2024 18:29:36 +0000</pubDate>
      <link>https://dev.to/fabon/publish-pure-esm-npm-package-written-in-typescript-to-jsr-4ih2</link>
      <guid>https://dev.to/fabon/publish-pure-esm-npm-package-written-in-typescript-to-jsr-4ih2</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You can publish a npm package to JSR&lt;/li&gt;
&lt;li&gt;JSR's API docs generation is delightful for a small OSS project, even if it doesn't use Deno&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://jsr.io/" rel="noopener noreferrer"&gt;JSR&lt;/a&gt; is a new package registry for JavaScript, which has compatibility to npm. JSR supports ES modules and TypeScript by default.&lt;/p&gt;

&lt;p&gt;I'm developing &lt;a href="https://jsr.io/@fabon/vremel" rel="noopener noreferrer"&gt;vremel&lt;/a&gt;, an utility library for &lt;a href="https://tc39.es/proposal-temporal/docs/" rel="noopener noreferrer"&gt;Temporal API&lt;/a&gt; (similar to &lt;a href="https://date-fns.org/" rel="noopener noreferrer"&gt;date-fns&lt;/a&gt; for Date). It's a pure ESM package&lt;sup id="fnref1"&gt;1&lt;/sup&gt; and written in TypeScript.&lt;/p&gt;

&lt;p&gt;The package is formerly published only to official npm registry, but recently I started to publish it to JSR &lt;strong&gt;in addition&lt;/strong&gt;. At first it was just for fun; I want to try a new thing. However, after I've published the package to JSR, I noticed that JSR saved me a headache: documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  API docs problem
&lt;/h2&gt;

&lt;p&gt;JavaScript (TypeScript) ecosystem has various types of API docs generators. Maybe the most popular one is &lt;a href="https://typedoc.org/" rel="noopener noreferrer"&gt;TypeDoc&lt;/a&gt;. While generating API docs itself is easy, hosting API docs is pretty hard. Publishing generated HTML to static hosting service like GitHub Pages is the method I adopted previously, but it's not an ideal solution because we can't view docs for older versions.&lt;/p&gt;

&lt;p&gt;We can generate docs for all previous versions each time a new version is released, or save generated docs of older versions to external storage. Yes, it's technically possible, but complicated.&lt;/p&gt;

&lt;p&gt;Now JSR have changed this situation. After publishing the package, we can view API docs of each version (similar to &lt;a href="https://docs.rs/" rel="noopener noreferrer"&gt;docs.rs&lt;/a&gt; in Rust or &lt;a href="https://pkg.go.dev/" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt; in Go). All we have to do is to write few lines of JSON. Optionally you can publish a package from GitHub Actions by adding only few lines to a workflow file. Any other setup (install packages, write config for document generator...) is not needed.&lt;/p&gt;

&lt;p&gt;example: &lt;a href="https://jsr.io/@fabon/vremel@0.3.3/doc" rel="noopener noreferrer"&gt;API docs for vremel v0.3.3&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcie0fb5joivv8nop9sek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcie0fb5joivv8nop9sek.png" alt="v0.3.3 docs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  npm packages in JSR
&lt;/h2&gt;

&lt;p&gt;JSR handles TypeScript source files very well, so publishing original TypeScript files is better than publishing generated JS and TypeScript definition files.&lt;/p&gt;

&lt;p&gt;If the project has &lt;code&gt;package.json&lt;/code&gt;, then &lt;code&gt;jsr publish&lt;/code&gt; command can resolve builtin modules of Node, npm packages listed in the &lt;code&gt;dependencies&lt;/code&gt; field of &lt;code&gt;package.json&lt;/code&gt;. Also it can handle a &lt;code&gt;.js&lt;/code&gt; extension in relative imports from &lt;code&gt;.ts&lt;/code&gt; files (called 'sloppy imports' in Deno), which is the rule of pure ESM npm packages written in TypeScript&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actual steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create a package
&lt;/h3&gt;

&lt;p&gt;Access &lt;a href="https://jsr.io/new" rel="noopener noreferrer"&gt;https://jsr.io/new&lt;/a&gt; and create a package.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write &lt;code&gt;jsr.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Write &lt;code&gt;jsr.json&lt;/code&gt;. See also &lt;a href="https://jsr.io/docs/package-configuration" rel="noopener noreferrer"&gt;JSR package config&lt;/a&gt;. For now you can specify subpath exports and files to be included or excluded.&lt;/p&gt;

&lt;p&gt;In my case:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@fabon/vremel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.3.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/index.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"./duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/duration/index.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publish"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"CHANGELOG.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"README.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"LICENSE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"src/**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"jsr.json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"**/*.test.ts"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Note that you have to include &lt;code&gt;jsr.json&lt;/code&gt; itself in &lt;code&gt;publish.include&lt;/code&gt;. Otherwise &lt;code&gt;jsr publish&lt;/code&gt; will fail (at least). In contrast, &lt;code&gt;package.json&lt;/code&gt; is not necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix 'slow types'
&lt;/h3&gt;

&lt;p&gt;You can run &lt;code&gt;npx jsr publish --dry-run&lt;/code&gt; to check whether the package is fine. You may encounter the error like &lt;code&gt;missing explicit return type in the public API&lt;/code&gt; due to JSR's restriction (See &lt;a href="https://jsr.io/docs/about-slow-types" rel="noopener noreferrer"&gt;About "slow types"&lt;/a&gt;). In most cases all you have to do is just clarify return types.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;//after&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can pass &lt;code&gt;--allow-slow-types&lt;/code&gt; to &lt;code&gt;jsr publish&lt;/code&gt; if you have to depend on slow types, although it's not recommended by JSR.&lt;/p&gt;

&lt;p&gt;In my case: &lt;a href="https://github.com/fabon-f/vremel/commit/50d2963ddbada084aaf0cd9d129f2059304c93d8" rel="noopener noreferrer"&gt;Clarify return types explicitly · fabon-f/vremel@50d2963 · GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up GitHub Actions
&lt;/h3&gt;

&lt;p&gt;Of course you can publish to JSR from your PC, but it's better to publish from CI. Fortunately publishing to JSR from GitHub Actions is super easy, just follow official guides.&lt;/p&gt;

&lt;p&gt;Note that if the package depends on external npm packages, you have to install dependencies to &lt;code&gt;node_modules&lt;/code&gt; before running &lt;code&gt;jsr publish&lt;/code&gt;. This is because &lt;code&gt;jsr&lt;/code&gt; command uses Node.js compatibility mode called &lt;a href="https://deno.com/blog/v1.38#nodejs-compatibility-improvements" rel="noopener noreferrer"&gt;BYONM&lt;/a&gt; when &lt;code&gt;package.json&lt;/code&gt; exist in the project.&lt;/p&gt;

&lt;p&gt;Example of workflow file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish the package&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v*"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jsr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish package&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx jsr publish&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After pushing a release tag, the package will be published to JSR from GitHub Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  API docs generation in JSR
&lt;/h2&gt;

&lt;p&gt;JSR uses Deno's document generator (&lt;a href="https://github.com/denoland/deno_doc" rel="noopener noreferrer"&gt;deno_doc&lt;/a&gt;), which is under development and doesn't support all JSDoc tags yet&lt;sup id="fnref3"&gt;3&lt;/sup&gt;. So your JSDoc comment can be showed differently from your intention. You can run &lt;code&gt;deno doc --unstable-sloppy-imports --html path/to/entrypoint.ts&lt;/code&gt; to check how generate docs will look.&lt;/p&gt;

&lt;p&gt;See also: &lt;a href="https://deno.com/blog/document-javascript-package" rel="noopener noreferrer"&gt;How to document your JavaScript package&lt;/a&gt; from Deno team blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;JSR is now under heavy development. I'm very looking forward to its rapid advance, especially in API docs generation.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Actually it's a dual package, but its structure and configurations are no different from a pure ESM package. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;These Node's style imports are rewritten to Deno's style internally by &lt;code&gt;jsr publish&lt;/code&gt; command before publishing. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;For example &lt;code&gt;@description&lt;/code&gt; and &lt;code&gt;@summary&lt;/code&gt; is not supported yet (in 2024-05-13); these tags will be simply ignored for now. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>github</category>
    </item>
  </channel>
</rss>
