Skip to content

JSR User Guide

About 4339 wordsAbout 14 min

tool

2024-03-10

jsr logo

introduce

Recently, Deno** released a Javascript Registry (JSR), a new JavaScript package registry. It is similar to npm, but is not a package management tool, but provides package storage services for package registration and package release.

It can be used with npm, yarn and pnpm, etc., and supports runtimes such as Node.js, Deno, Bun and browser.

The goal of JSR is not to replace NPM, but to be a superset of NPM, which is more in line with the needs of modern JavaScript developers and improve the developer's experience. And get better support in all aspects such as reliability, security, and performance.

Using JSR

You can use JSR packages in any runtime that supports ES modules, such as Deno, Node.js, Bun, Cloudflare Workers, etc. You can also use JSR packages with build tools that support ES modules, such as Vite, esbuild, Webpack, and Rollup.

You can import the JSR package into your project using any of the following commands:

#deno
deno add @luca/cases

# npm (One of the following options depends on your package manager)
npx jsr add @luca/cases
yarn dlx jsr add @luca/cases
pnpm dlx jsr add @luca/cases
bunx jsr add @luca/cases

If you use the Deno, the deno add command will generate the specified JSR module to add the import map entry in the deno.json file:

deno.json
{
 "imports": {
 "@luca/cases": "jsr:@luca/cases@^1.0.1"
 }
}

For npm and npm-compatible package managers, the jsr command adds dependencies to your package.json file, And add the .npmrc file to your project root directory, which contains the configuration you need to use JSR with npm.

package.json
{
 "dependencies": {
 "@luca/cases": "npm:@jsr/luca__cases@^1.0.1"
 }
}

This npm dependency configuration uses a special custom scope called @jsr, Automatically add the following configuration in .npmrc:

.npmrc
@jsr:registry=https://npm.jsr.io

Note

You should add .npmrc to your source control to use it later when installing or updating the JSR package.

Why choose JSR?

Node.js's great success is largely due to the success of NPM. NPM has 2 million+ (may reach 3 million in the future) packages, Probably the most successful package manager and registry in history. This is an achievement that the JavaScript community is proud of.

So, since there is already an NPM, why do you still need to build a JSR? Because in today's world, there have been earth-shaking changes from when NPM was introduced:

  • **ECMAScript module has become the standard. ** The web platform now uses ESM as its preferred module format, replacing CommonJS.
  • **In addition to Node.js and Browser, there are more JavaScript runtimes. ** With Deno, Bun, workerd, and other new JavaScript environments emerging, the Node.js-centric package registry is no longer meeting the needs.
  • Typescript has become the de facto standard. TypeScript, as a test platform for JavaScript superset and the latest ECMAScript features, has become the choice of many important JavaScript libraries. Modern registries should be designed with TypeScript in mind.

Here are the reasons why you consider using JSR:

Native Typescript Support

JSR is designed with TypeScript support in mind. TypeScript source files can be published directly to JSR. These files can be used directly on platforms that support TypeScript (such as Deno).

For other environments that lack native support for TypeScript (such as Node.js), JSR converts the source code to JavaScript and generates a d.ts type declaration file. TypeScript tools to support Node.js projects. Module authors do not require additional configuration or build steps.

JSR will also generate reference documents for packages from TypeScript source code, providing rich online documentation that you can maintain with your code.

ESM modules only

The web standard for JavaScript modules is ESM. A modern package registration center should unite around this standard and turn the community in that direction. Therefore, JSR is designed for ESM only.

Cross-runtime support

The goal of JSR is to be able to work anywhere JavaScript works and provide a runtime-independent registry for JavaScript and TypeScript code. Today, JSR can be used with Deno and other NPM environments including node_modules. This means that Node.js, Bun, Cloudflare Workers, and other projects that use the package.json management dependencies can also interoperate with JSR.

JSR is a superset of NPM

JSR is a superset of npm, just as TypeScript is a superset of JavaScript.

JSR is designed to interoperate with npm-based projects and packages. You can use the JSR package in any runtime environment that uses the node_modules folder. The JSR module can import dependencies from npm.

Excellent development experience

JSR has many features designed to help module publishers improve productivity, including but not limited to:

  • Easily publish with a single command - CLI will walk you through the rest
  • Automatically generate API documentation from source code
  • Zero configuration publishing from GitHub Actions
  • Automatically include .d.ts files distributed by Node.js/npm
  • Automatic guidance on TypeScript best practices will make your code load as fast as possible
  • More...

Fast, safe and reliable

JSR is designed to be safe, fast, flexible, and also works well in resource-constrained environments.

  • JSR uses global CDN to provide packages and uses local cache and high parallelism to speed up downloads.
  • JSR package uploads are immutable, so you can trust that the package will never change or disappear at your bottom after downloading.
  • JSR package downloads are very efficient and only download the exact file you are importing.
  • JSR publishes packages from the CI using OIDC-based authentication and publishes from the local computer using the token-free interactive authentication process.

JSR package rules

All packages uploaded to JSR are automatically processed and verified during the release process to ensure that all code hosted on the JSR adheres to a consistent set of rules. These rules are designed to achieve portability across environments. The code must follow these rules before it can be published to the JSR.

Only ESM modules are supported

The JSR package only supports ESM modules, which means you can only publish modules that use the import and export keywords. Unable to publish CommonJS modules

Support NPM packages

You can depend on npm packages by specifying them in dependencies of package.json, Or use the npm: specifier to reference them in your code, for example as import { cloneDeep } from "npm:lodash@4"; .

Support JSR packages

Relying on JSR packages by specifying them in dependents of package.json, Or use the jsr: specifier to reference them in your code, for example import { encodeBase64 } from "jsr:@std/encoding@1/base64"; .

Support node: built-in functions

Use the node: scheme to import Node.js built-in functions. For example, you can use import { readFile } from "node:fs"; to import the fs module. If the package has package.json, You can also import Node.js built-in programs using the naked specifier (without the node: prefix).

Simple file name

The file name must be compatible with Windows and Unix. This means that the file name cannot contain characters such as *, : or ?. Multiple files with the same name but different upper and lower cases should be avoided.

It is best not to use TypeScript "slow types"

To speed up type checking, support for document generation and Node.js compatibility, JSR packages should not use certain TypeScript types in exported functions, classes, or variables. This is enforced by default, but can be optionally ignored.

Effective cross-file import

All relative imports between modules in a package must be parsed at release. The format of supported specifiers depends on whether to use package.json.

Publish package to JSR

You can publish most JavaScript and TypeScript code written using ESM modules as JSR packages. The JSR package is published to jsr.io and can be imported from Deno, Node.js, and other tools.

Code written for the runtime using package.json and code written for Deno can be published as packages to JSR. JSR supports and encourages the release of TypeScript source code instead of paired .js + .d.ts files. This allows JSR to provide more useful automated documentation and helps provide improved autocomplete functionality in the editor.

包配置文件

你必须在你的包中添加一个 包配置文件,该文件名为 jsr.json。文件中包含包括 包的元数据,如 包名、 版本号、入口点。Deno 用户还可以在其 deno.json 中包含所需的属性,以避免创建另一个文件。

jsr.json / deno.json
{
  "name": "@luca/greet",
  "version": "1.0.0",
  "exports": "./mod.ts"
}

Create scope and package

To package to JSR, you must first create a scope, and then create a package under this scope. scope is similar to NPM scope, scope starts with the @ symbol followed by the name, such as @luca is a

You can create scope in jsr.io/new. The name length of scope must range from 2 to 32 characters. Only lowercase characters, hyphens and numbers are used. Only if the name is not used, names that are very similar to the existing scope are prohibited.

After creating the scope, you can create a package in this scope. The package name must be between 2 and 20 characters long. And can only contain lowercase letters, numbers and hyphens. Only if the name is not used, names that are very similar to existing packages are prohibited.

jsr new

Verification package

To publish a package (including performing a trial run to confirm that the package meets all JSR rules), Requires jsr publish or deno publish. The syntax of these two commands is roughly the same. Depending on the different tools, the issuance command can be called as follows.

#deno
deno publish
# npm
npx jsr publish
# yarn
yarn dlx jsr publish
# pnpm
pnpm dlx jsr publish

You can run jsr publish with the --dry-run flag to perform all publish verifications that occur during the actual publishing period. This will print out the list of files to be published, but will not actually be published to the registry.

#deno
$ deno publish --dry-run
# npm
$ npx jsr publish --dry-run
# yarn
yarn dlx jsr publish --dry-run
# pnpm
pnpm dlx jsr publish --dry-run

Publish packages from this machine

Use the jsr publish or deno publish command to publish packages from your local computer.

Authentication is done through a browser, so there is no need to provide any authentication information to the JSR cli.

Go to the root directory of the package (including the jsr.json / deno.json file), and run ​​ jsr publish.

#deno
$ deno publish
# npm
$ npx jsr publish
# yarn
yarn dlx jsr publish
# pnpm
pnpm dlx jsr publish

During release, both the JSR CLI and the JSR server will run many checks on the package to ensure it is valid. If any of these checks fail, the CLI will output an error message. These errors must be fixed before attempting to publish again.

Publish packages from GitHub Actions

To publish a package from Github Actions, you must first link the package to Github Repo in the Setting tab of the package page of the JSR.

jsr link

Then, in the Github Repo, add a workflow configuration file, such as .github/workflows/publish.yml.

.github/workflows/publish.yml
name: Publish

on:
 push:
 branches:
 - main

jobs:
 publish:
 runs-on: ubuntu-latest
 permissions:
 contents: read
 id-token: write # The OIDC ID token is used for authentication with JSR.
 Steps:
 - uses: actions/checkout@v4
 - run: npx jsr publish

This workflow runs every time it is pushed to the main branch of the repository. It will publish your package to the JSR and automatically use the correct version number based on the version in the jsr.json file. If the version specified in the jsr.json file has been published to JSR, jsr publish will not attempt to publish.

Filter files

jsr publish will ignore the files listed in the .gitignore file in the package root directory. Additionally, the include and exclude fields can be specified in the jsr.json / deno.json file to include, Ignore or cancel gitignore specific operation files.

For example, to include only certain files selectively, you can use the include option to specify a glob that matches all files:

jsr.json
{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": "./src/mod.ts",
 // note: this will be collapsed down to just include in the future
 "publish": {
 "include": ["LICENSE", "README.md", "src/**/*.ts"]
 }
}

You can also exclude certain files through the exclude option:

jsr.json
{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": "./src/mod.ts",
 "publish": {
 "include": ["LICENSE", "README.md", "src/**/*.ts"],
 "exclude": ["src/tests"]
 }
}

Cancel ignoring files when "include" is not used

You may have a package containing the .gitignore file that contains the following:

.gitignore
.env
dist/

In this case, all files in the dist/ directory and any files named .env are ignored when publishing.

However, if you want to publish the dist/ directory, this can be inconvenient because there is "exports" pointing to it (or its subdirectories). In this case, the dist/ directory can be unignited by using negative in the exclude field in the jsr.json / deno.json file.

jsr.json
{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": "./dist/mod.ts",
 "publish": {
 "exclude": ["!dist"]
 }
}

In this case, the dist/ directory will be included when publishing, even if it is listed in the .gitignore file.

jsr.json file

A JSR package needs to include a configuration file that specifies the name, version, and export of the package. The file should be named jsr.json or jsr.jsonc. When using Deno, all properties of the jsr.json configuration file can be placed in deno.json.

jsr.json / deno.json
{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": "./mod.ts"
}

name

The name field is the name of the package, prefixed with JSR scope.

version

The version field is the version of the package and it must be a valid version of SemVer. Each time a new version is released, the package version must be added.

exports

The exports field tells the user of the JSR package which modules should be imported. The exports field can be specified as a single string or as an object that maps the entry point name to the path in the package.

{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": {
 ".": "./mod.ts",
 "./greet": "./greet.ts"
 }
}

In the example above, the exports field is an object. The . entry point is the default entry point for the package. The ./greet entry point is a named entry point. Through this entry point, you can import the greet.ts module using import { greet } from "@luca/greet/greet";, Use import { greet } from "@luca/greet"; to import the mod.ts module.

You can also specify the exports field as a single string. This is very useful if there is only one entry point in the package. It is semantically equivalent to specifying the default entry point in object form.

{
 "name": "@luca/greet",
 "version": "1.0.0",
 "exports": {
 ".": "./mod.ts"
 }
 "exports": "./mod.ts"
}

include and exclude

You can also use the include and exclude options to include and exclude files during the publishing process. When using deno.json, you can use publish.include and publish.exclude to include and exclude files that are only used for publication. Instead of using all Deno subcommands. Check out [Filter Files] (#Filter Files) to learn more.

Write a document

Writing a document is critical to the success of a package. JSR makes it easy for package authors to get excellent documentation because it generates documentation based on JSDoc annotations in package source code.

The generated document is displayed on the package page. The document will also be displayed to the user in the editor in the form of a complete and hover description.

There are two important parts of the document:

  • symbol documentation: This is the documentation for each individual function, interface, constant or class exported by the package.
  • module documentation: This is the documentation for each exported module in the package - it acts as an overview or summary of all symbols in the module.

symbol documentation

Add a JSDoc comment for each exported function, interface, constant, or class.

/** // [!code ++]
 * This function takes two numbers as input, and then adds these numbers using
 * floating point math.
 */
export function add(a: number, b: number): number {
 return a + b
}

For functions, you can add a document to a specific parameter or return type:

/**
 * Search the database with the given query.
 *
 * @param query This is the query to search with. It should be less than 50 chars to ensure good performance. // [!code ++]
 * @param limit The number of items to return. If unspecified, defaults to 20. // [!code ++]
 * @returns The array of matched items. // [!code ++]
 */
export function search(query: string, limit: number = 20): string[]

For more complex ones, it is usually best to include an example that demonstrates how to use the function:

/**
 * Search the database with the given query.
 *
 * ```ts
 * search("Alan") // ["Alan Turing", "Alan Kay", ...]
 * ```
 */
export function search(query: string, limit: number = 20): string[]

interfaces can also be annotated using JSDoc. Their properties and methods can also be commented:

/** The options bag to pass to the {@link search} method. */
export interface SearchOptions {
 /** The maximum number of items to return from the search. Defaults to 50 if
 * unspecified. */
 limit?: number
 /** Skip the given number of items. This is helpful to implement pagination.
 * Defaults to 0 (do not skip) if not specified. */
 skip?: number

 /** The function to call if the {@link search} function needs to show warnings
 * to the user. If not specified, warnings will be silently swallowed. */
 reportWarning?(message: string): void
}

Classes can be similarly annotated as interfaces and functions:

/**
 * A class to represent a person.
 */
export class Person {
 /** The name of the person. */
 name: string
 /** The age of the person. */
 age: number

 /**
 * Create a new person with the given name and age.
 * @param name The name of the person.
 * @param age The age of the person. Must be non-negative.
 */
 constructor(name: string, age: number) {
 if (age < 0) {
 throw new Error('Age cannot be negative')
 }
 this.name = name
 this.age = age
 }

 /** Print a greeting to the console. */
 greet() {
 console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`)
 }
}

module documentation

Adding JSDoc comments to modules is useful for providing an overview of modules and their export symbols.

To record a module, add a JSDoc annotation at the top of the module file and include the @module tag anywhere in the comment:

/**
 * This module contains functions to search the database.
 * @module
 */

/** The options bag to pass to the {@link search} method. */
export interface SearchOptions {}

/** Search the database with the given query. */
export function search(query: string, options?: SearchOptions): string[];

You can also include examples in the module documentation:

/**
 * @module
 *
 * This module contains functions to search the database.
 *
 * ```ts
 * import { search } from "@luca/search";
 *
 * search("Alan") // ["Alan Turing", "Alan Kay", ...]
 * ```
 */

Slow Types

Many of the features of JSR analyze source code, especially TypeScript types in the source code. This is done to generate documents, generate type declarations for the npm compatibility layer, and to speed up type checking for Deno projects using packages in JSR.

For these functions to work, the TypeScript source must not export any function, class, interface, or variable or type alias that itself or reference "Slow Types". "Slow Types" are types that are not explicitly written, or are too complex to require a lot of reasoning to understand.

For these features, it is too expensive for JSR to perform such inferences, so the public API does not support these types.

Warning

If JSR finds "Slow Types" in the package, some features will not work or have degraded quality. include:

  • Type checking of package users will be slower. For most packages, the speed will be reduced by at least 1.5-2 times. It could be much higher.
  • This package will not be able to generate type declarations for the npm compatibility layer, or "Slow Types" in the generated type declaration will be omitted or replaced with any .
  • The package will not be able to generate the document for the package, or the generated document will be omitted or details will be missing.

What is "Slow Types"?

"Slow Types" appears when functions, variables, or interfaces are exported from packages, and their types are not explicitly written, or are too complex to be simply inferred.

example:

// There is a problem with this function because the return type is not explicitly written, so it must be inferred from the function body.
export function foo() {
 return Math.random().toString()
}
const foo = 'foo'
const bar = 'bar'
export class MyClass {
 // This property is problematic because the type is not explicitly written, so it must be inferred from the initialization item.
 prop = foo + ' ' + bar
}

Slow Types inside the package (i.e. not exported) will not have any problems with JSR and package consumers.

TypeScript Limitations

This section lists all the restrictions imposed by the "no slow types" policy on TypeScript code:

  1. All exported functions, classes, variables, and types must have explicit types. For example, a function should have an explicit return type and a class should have an explicit property type.
  2. Module enhancement and global enhancement shall not be used. This means that the package cannot use declare global, declare module, or export as namespace to expand the global scope or other modules.
  3. The CommonJS function must not be used. This means that the package cannot use export = or import foo = require("foo") .
  4. All types in exported functions, classes, variables, and types must be simply inferred or explicit. If the expression is too complex to infer, its type should be explicitly assigned to the intermediate type.
  5. Deconstruction in export is not supported. Export each symbol separately, not deconstruct.
  6. Types must not refer to private fields of a class.

Explicit Type

All symbols exported from packages must explicitly specify the type. For example, a function should have an explicit return type:

export function add(a: number, b: number) {
export function add(a: number, b: number): number {
 return a + b;
}

The attributes of a class should have clear types:

export class Person {
 name
 age
 name: string
 age: number
 constructor(name: string, age: number) {
 this.name = name
 this.age = age
 }
}

Global Enhancement

Module enhancements and global enhancements are not allowed. This means that the package cannot use declare global to introduce new global variables, or declare module to augment other modules.

Here are some unsupported code examples:

declare global {
 const globalVariable: string
}
declare module 'some-module' {
 const someModuleVariable: string
}

CommonJS Features

CommonJS features are not allowed. This means that the package cannot use export = or import foo = require("foo") .

Use ESM syntax instead:

export = 5
export default 5
import foo = require('foo')
import foo from 'foo'

Type must be simple inference or explicit

All types in exported functions, classes, variables, and types must be simply inferred or explicit. If the expression is too complex to infer, its type should be explicitly assigned to the intermediate type.

For example, the default exported type is too complex to infer, so it must be explicitly declared using an intermediate type:

class Class {}

export default {
 test: new Class(), 
}
const obj: { test: Class } = {
 test: new Class(), 
}
export default obj

Or use assertion:

class Class {}

 export default {
 test: new Class(),
};
} as { test: Class };

For superclass expressions, evaluate the expression and assign it to the intermediate type:

interface ISuperClass {}

function getSuperClass() {
 return class SuperClass implements ISuperClass {}
}

export class MyClass extends getSuperClass() {}
const SuperClass: ISuperClass = getSuperClass()
export class MyClass extends SuperClass {}

Export without destruction

Deconstruction in export is not supported. Instead of using deconstruction, export each symbol separately:

export const { foo, bar } = { foo: 5, bar: 'world' }
const obj = { foo: 5, bar: 'world' }
export const foo: number = obj.foo
export const bar: string = obj.bar

Types must not refer to private fields of a class

During the inference process, the type must not refer to the private fields of the class.

In this example, the public field refers to the private field, which is not allowed.

export class MyClass {
 prop!: typeof MyClass.prototype.myPrivateMember
 private myPrivateMember!: string
 prop!: MyPrivateMember
 private myPrivateMember!: MyPrivateMember
}

type MyPrivateMember = string

package score

JSR scores are metrics that JSR automatically assigns to each package based on certain factors indicating package quality. This score is used to rank software packages in search results, helping users understand the quality of software packages at a glance.

The JSR score is a percentage between 0 and 100, calculated based on factors in 4 advanced categories:

  • Document: There are readme files, module documents, and documents of common functions and types.
  • Best Practice: Packages should not use SLow Types and should be published with the package source.
  • Discoverability: The package should have a description to help users find the package through searches.
  • Compatibility: The package should have at least one runtime marked as "compatible" in the Runtime Compatibility section of the package page. Additionally, packages are rewarded for having multiple compatible runtimes.

Each category has different specific factors that affect scores. Each factor has different weights.