TLDR: This article discusses how to organize files in a repository for publishing a Deno Module, focusing on code-splitting dependencies and having different entry points within the module.
Recently, I've been working on creating small Deno packages for personal use and sharing with others. The repositories I've been working with mainly consist of command line tools or webpage routes, or sometimes a combination of both. One of my recent projects called Deno Resume presented an interesting challenge. The core of the module is to generate an HTML page from a JSON prop object using JSX and preact-render-to-string under the hood, similar to how Fresh works. The website doesn't have any interactivity, so it's essentially a generator for a single HTML string. When it came time to publish the package, I encountered some scope creep. I wanted to use the Deno Puppeteer module to programmatically generate a PDF of the resume. However, distributing the Puppeteer dependency in an unwise manner could potentially add unnecessary weight to the entire project. Unlike the days of NPM and package.json, Deno takes a different approach. This is the future. So, let's explore our options.
Distributing a Deno Module
It's common practice for Deno modules to use mod.ts
at the root of the project as the primary export or "entry point" of the module. This practice is borrowed from Rust and outlined in the Deno (😒 internal) style guide. However, in our case, it's unwise to export all of the functions within our module here, functions like the aforementioned toPdf
function, because that would mean users would need to download the Puppeteer dependency every time, even if they are not interested in the PDF functionality. Furthermore, it would cause compatibility issues with Deno Deploy.
For me, the key is to think of individual TypeScript files as a series of functions with the same dependencies. Rather than trying to cram everything into one module, it's important to think critically about the dependency tree.
Even after separating things in a reasonable way, I was still stuck. I had three consumables I was considering as part of the module's "distribution": pdf.ts
, serve.ts
, and render.ts
. These files enabled programmatic functionality of the code I had written. The question was: where should these files live? Should they be placed at the root of the repository or within a dist
folder? To make matters more complicated, I also wanted to provide a command-line interface (CLI), which introduced a host of other issues.
The Folder Structure
Below is a diagram of the folder structure for Deno Resume. The shift in thinking that I needed was to consider the fundamental nature of URL imports. The key is to keep the URLs as short as possible. This improves discoverability and simplifies navigation. The root of your package should be where consumers access the module, not some nested dist
, src
, or lib
folder. I like to think of code within src as "private," similar to "private" methods on a class. They are still accessible in this case, but there is no intention for the end user to view or use them.
This creates a delightful experience when using the module. For instance:
- https://deno.land/x/resume/pdf/cmd.ts
- https://deno.land/x/resume/pdf/mod.ts
- https://deno.land/x/resume/serve/cmd.ts
- https://deno.land/x/resume/serve/mod.ts
- https://deno.land/x/resume/cmd.ts
- https://deno.land/x/resume/mod.ts
I can't stress enough how many times I've tried route-level files like pdf_cmd.ts
or cmd_pdf.ts
with mod_pdf.ts
or pdf_mod.ts
. I find this approach quite unappealing 🤮. It doesn't look good, it doesn't feel good, and it's hard to remember. The beauty of embracing URLs is the ability to use "directory prefixing" and keep the file names short.
Here's the structure:
.
├── src
│ ├── component.tsx
│ ├── html.tsx
│ ├── pdf.ts
│ ├── render.ts
│ ├── serve.ts
│ ├── twind
│ │ ├── attributes.ts
│ │ ├── mod.ts
│ │ ├── parse.ts
│ │ └── print.ts
│ ├── types.ts
│ └── util.ts
├── pdf
│ ├── cmd.ts
│ └── mod.ts
├── render
│ ├── cmd.ts
│ └── mod.ts
├── serve
│ ├── cmd.ts
│ └── mod.ts
├── cmd.ts
├── all.ts
└── mod.ts
In the case of Deno Resume, every file outside of src is a thin wrapper for something within src. These files don't contain any "business logic." They simply provide small entry points.
On mod.ts
In our case, mod.ts
serves as the "entry point" for our module, but it doesn't include all the functions in the project. It doesn't include serve
or pdf
related code because they are out of scope for the original module. If you want to use those functionalities, you can opt into them by using pdf/mod.ts
or serve/mod.ts
, respectively. I did add an all.ts
file that exports every function and installs all dependencies, but it deviates from the convention, and to be honest, I don't think it will be used much.
Creating a CLI Tool with Conditional dependencies
To get straight to the point, the answer is dynamic imports. The secret to creating a CLI with conditional dependencies lies in dynamic imports. What does this mean? Well, if I want to create a CLI tool with three commands (pdf
, serve
, and html
), but I don't want to load the dependencies specific to pdf
unless they're needed and called, how do I achieve that?
import { commandInput } from "./src/util.ts";
const { flags } = await commandInput()
if (flags.pdf) {
await import('./pdf/cmd.ts')
} else if (flags.html) {
await import('./render/cmd.ts')
} else if (flags.serve) {
await import('./serve/cmd.ts')
} else {
throw new Error('no command specified')
}
This gives you a glimpse into how the overall folder structure works and the secret to organizing the files that the Deno module consumer will use.
Conclusion
Coming from Node.js and building NPM packages, there is a slight shift in how you need to think about the entry point of your module. Gone are the days when you would include a dependency in package.json
and users had to install it, even if it was an "optional" dependency. With Deno, it is now conceivable to create a module without adding unnecessary dependencies. It feels somewhat luxurious.
Feel free to comment below with any ideas, thoughts, or feedback! I would love to hear from other developers who have created Deno modules and discover their best practices.
Top comments (0)