Skip to content

Hey Deno! Where is my package.json?

Disclaimer: This blog was written for Deno versions prior to 1.3.1. After 1.3.1, while Deno can handle projects that contain a package.json (to help facilitate migration), it is still recommended that you handle your dependencies as discussed in this article.

Introduction

Where is my package.json?

That was one of my first questions when I started learning Deno. Coming from a NodeJS background, I am used to having a package manager (such as NPM) for managing dependencies, scripts and other configurations, and I’m used to using the package.json file to declare my dependencies and their versions.

Deno has no concept of a package manager, as external modules are imported directly into local modules (e.g import { bold } from ‘https://deno.land/std@v0.32.0/fmt/colors.ts’). At first, this seems very convenient, but it got me wondering how I would be able to manage and update dependencies when they are imported into several different modules across a large project. And how about running scripts? Node allows you to define arbitrary scripts in the package.json that can be executed using npm run. How do we define and manage scripts in Deno?

In this article, we will discuss the various ways to manage dependencies in Deno, and also how to manage all scripts needed for our project.

Managing Dependencies

Using deps.ts

The standard practice for managing dependencies in Deno is to create a deps.ts file that imports, and then immediately re-exports all third-party code.

/**
 * deps.ts
 * Exports all project dependencies from the file.
 */
export * as log from 'https://deno.land/std@0.167.0/log/mod.ts';
export { Application, Router, Context } from 'https://deno.land/x/oak@v11.1.0/mod.ts';
export type { Middleware } from 'https://deno.land/x/oak@v11.1.0/mod.ts';
export { DataTypes, Database, Model, PostgresConnector } from 'https://deno.land/x/denodb@v1.1.0/mod.ts';
export { oakCors } from 'https://deno.land/x/cors@v1.2.2/mod.ts';
export { applyGraphQL, gql, GQLError } from 'https://deno.land/x/oak_graphql@0.6.4/mod.ts';

In your local module, these methods, and classes can be referenced from the deps.ts file.

import { Application, applyGraphQL, oakCors, Router } from '../deps.ts';

You may be familiar with the concept of dev dependencies in NPM. You can define dev dependencies in Deno using a separate dev_deps.ts file, allowing for a clean separation between dev-only and production dependencies.

With this approach, managing dependencies in Deno becomes much simpler. For example, to upgrade a dependency version, you make the change in the depts.ts file, and it propagates automatically to all modules in which it is referenced.

When using this approach, one should consider integrity checking & lock files. This is basically Deno’s solution for avoiding production issues if the content in the remote url (e.g https://some.url/a.ts) is changed. This could lead to the production module running with different dependency code than your local module.

Just like package-lock.json in NodeJS, Deno can store and check subresource integrity for modules using a small JSON file. To autogenerate a lock file, create a deno.json file at the root of your project and a deno.lock file will be autogenerated.

You can also choose a different file name by updating the deno.json file like so:

{
  "lock": "./lock.json"
}

You can also disable automatically creating, and validating a lock file by specifying:

{
  "lock": false
}

We can manually create or update the lock file using the Deno cache command, and --lock and --lock-write flags like so:

deno cache --lock=deno.lock --lock-write deps.ts

Then a new collaborator can clone the project on their machine and run:

deno cache --reload --lock=deno.lock deps.ts

Using import_map.json

Another way to manage dependencies in Deno is by using the import_map.json file.

This method is useful if you want to use "bare-specifiers" (specifiers without an absolute or relative path to them, e.g import react from ‘react’).

This file allows you to alias a specific import URL or file path, which can be useful if you want to use a custom alias for a dependency.

To use the import_map.json file, you first need to create it in the root directory of your project. The file should contain a JSON object with a single key, "imports", which maps import aliases to fully-qualified module URLs or file paths. You can use the import_map.json file to map import paths to remote dependencies and even NPM specifiers.

You can also use the import_map.json file to map aliases to local file paths. For example, if you have a local module in your project at ./src/lib/my_module.ts, you can map the import path "my_module" to this file.

Here's an example of an import_map.json file:

{
  "imports": {
    "lodash": "npm:lodash@^4.17",
    "react": "https://cdn.skypack.dev/react",
    "my_module": "./src/lib/my_module.ts"
  }
}

With this import_map.json file in place, you can now import the libraries using their aliases:

import lodash from 'lodash';
import react from 'react';
import { myFunction } from 'my_module';

console.log(lodash.defaults({ 'a': 1 }, { 'a': 3, 'b': 2 }))

Using the import_map.json file can be a convenient way to manage dependencies in your Deno projects, especially if you want to use custom aliases for your imports. Just be sure to include the --import-map flag when running your Deno application, like so:

deno run --import-map=./import_map.json main.ts

This will ensure that Deno uses the import map specified in the import_map.json file when resolving dependencies.

Managing Command Scripts

Like npm run, the Deno CLI also has a run command that is used to run scripts in files. Depending on the permission needed or the type of operation, there are certain flags that need to be passed to the run command. For example, if you want to run a web server that uses an env file and reads from it, your command will look like this:

deno run --allow-net --allow-env --allow-read main.ts

If we are reading and writing to a file, we need to add the --allow-write, or if we have an API that needs information about the operating system of the user, then we will also need to add --allow-sys. This can go on and on. We could decide to use --allow-all to accept all permissions, but this is not advisable.

The good news is that we can manage all our command scripts without having to retype them every time. We can add these scripts to the deno.json file.

The deno.json file is a configuration file for customizing basic deno CLI commands like fmt, lint, test etc. In addition, the file can also be used to define tasks using the "tasks" field. To define tasks, you can specify a mapping of task names to “tasks”. For example:

{
 "tasks": {
    "start-web": "deno run --watch --allow-net --allow-env --allow-read ./src/main.ts",
    "generate-type-definition": "deno run --allow-net --allow-env --allow-read --allow-write --allow-sys ./tools/generate_type_definition.ts"
 }
}

You can then run these tasks using the Deno task command, and specifying the task name, like this:

deno task start-web
deno task generate-type-definition

Conclusion

In this article, we saw how to manage dependencies and command scripts in a Deno project. If you’re coming from a Node background, and were confused about where the package.json file was, we hope this clarified how to accomplish some of the same things using the depts.ts, dev_depts.ts, import_map.json, and deno.json files.

We hope this article was helpful, and you were able to learn and be more comfortable with using Deno for your projects. If you want to learn more about Deno, check out deno.framework.dev for a list of libraries and resources. If you are looking to start a new Deno project, check out our starter kit resources at starter.dev.