zettca
zettca5mo ago

Deno JSR API package + CLI entrypoint "best-practices"

Hey 👋 I have a package that is both a CLI and provides a JS API. Previously with deno.land/x, the JS API entrypoint would be in mod.ts and the CLI entrypoint in main.ts; something like:
import myThing from "https://deno.land/x/myThing/mod.ts"
import myThing from "https://deno.land/x/myThing/mod.ts"
deno run https://deno.land/x/myThing/main.ts arg1 arg2
deno run https://deno.land/x/myThing/main.ts arg1 arg2
But I'm not sure of the best way to do it with JSR. Should there be multiple entrypoints? It's a small library It feels like the usage be something like this (single mod.ts entrypoint):
import myApi from "jsr:@scope/myThing"
import myApi from "jsr:@scope/myThing"
deno run jsr:@scope/myThing arg1 arg2
deno run jsr:@scope/myThing arg1 arg2
I see a few options: 1. Have the mod.ts entrypoint exports both the JS API and the CLI (under import.meta.main) 2. Have multiple entrypoints, but the imports won't be "pretty" (eg. jsr:@scope/myThing/cli.ts) 3. Separate the CLI into a separate package 3. seems a bit overkill, but 1. doesn't feel too right either What do you think? Thanks!
2 Replies
Deno AI Helper
Deno AI Helper5mo ago
I wasn't able to find the answer to your question in the official Deno documentation. However, based on the information you've provided, here are a few thoughts: If you're considering the best practices for structuring a Deno package that includes both a CLI and a JS API, it's important to think about the user experience and the separation of concerns. Here are some general considerations: 1. If the CLI functionality is a thin wrapper around the API, it might make sense to include both in the same module and use import.meta.main to determine if the module is being run as a script. This way, users who only want to use the API won't have to load any CLI-specific code when they import your module. 2. If the CLI has a significant amount of logic separate from the API, or if you anticipate it growing more complex over time, it might be cleaner to maintain separate entry points. This can help keep the API clean and focused, and it allows users to only load what they need. 3. Separating the CLI into a separate package could be considered overkill for a small library, but it does offer the cleanest separation. This might be more appropriate if the CLI and the API are likely to evolve independently or if they have different sets of dependencies. The advice provided here is not based on the Deno documentation, so please double-check the accuracy of this advice. It's also a good idea to consider the conventions and practices within the Deno community and to look at how other similar packages are structured for additional guidance.
CodyC
CodyC5mo ago
I have Opinions but, of course, everything depends on your use case(s).
1. Have the mod.ts entrypoint exports both the JS API and the CLI (under import.meta.main)
This is my preferred path, for ease-of-use for users. They gete a short package path, and deno install will likely install with a name that makes sense for your package. I assume by "under import.meta.main" you mean that you have a call to main() in your mod.ts that's guarded by an if (import.meta.main) {. 👍 You may want to export your main() function too, so folks can execute it from a script, instead of having to execute it as a standalone process. Depends on your use case. (ex: that might be less useful if your CLI is very interactive.) If you're also exposing an API, that may be a better way for them to use it.
2. Have multiple entrypoints, but the imports won't be "pretty" (eg. jsr:@scope/myThing/cli.ts)
JSR exports can be prettier than that. You can have an export "cli" which maps to "cli.ts" internally. The downside here is that, I think if you deno install that, you're going to get a script called cli which isn't very user-friendly. 1 & 2 don't have to be exclusive. You can do both. Have a default (./) entrypoint which can act as main and exposes the API, and a separate /api export which only gives the API for those that want a smaller dependency graph.
3. Separate the CLI into a separate package
Yeah, this seems like overkill until your project gets quite large. Managing two(+) separate packages that are tightly coupled is a headache.