The Deno Toolchain

The deno init Command

Similar to Node.js projects where we get a project going with e.g. npm init, Deno provides an init command to set up the essential files in your directory. Here’s how you can use it:

deno init my-deno-project

When run without arguments (deno init), it creates three default files in your current directory. If you provide an additional argument (like my-deno-project in the example above), Deno will create a folder with that name and place these files inside.

The Three Default Files

Let’s see what Deno creates for us:

  1. main.ts - This is the entry point of your app, similar to index.js or app.js in Node.js projects. It’s where your application’s execution begins. However, you’re not restricted to this name and can choose freely.

  2. main_test.ts - As the _test part suggests, this is where your tests live. Deno has built-in testing capabilities, which we’ll cover in more detail later. This file contains a basic test for the functionality in main.ts. It’s good to give the test files the same name as the file that is tested.

  3. deno.json - This is the configuration file for a Deno project, similar to package.json in Node.js, but with key differences. Here, you can configure various aspects of your project:

    • Import maps
      • Scripts
      • Formatter settings
      • Linter rules
      • TypeScript compiler options

    The deno.json file is central to customizing your Deno development environment.

Running a Deno Project

Once we’ve initialized our project, we can run it using the deno run command:

deno run main.ts

Give the files created by default this command will execute our main.ts file with Add 2 + 3 = 5 printed to the console.

One key difference from Node.js is that Deno doesn’t require a package manager or a node_modules folder. All dependencies are fetched and cached on first run, making project setup much simpler. However if we would like to use a npm package this possible. How it’s done is described in the next chapter about using external modules in Deno.

📋 Further Documentation:

Working with External Modules in Deno

Deno has a unique and refreshing approach to including modules and external code. This approach simplifies dependency management and makes your projects more portable.

URL-based Imports

One of the standout features of Deno is its ability to import modules directly from URLs. You can pull code straight from GitHub, or any other source that serves JavaScript or TypeScript files, without needing to install it first. Here’s an example:

import { format } from "https://raw.githubusercontent.com/denoland/std/main/fmt/duration.ts";
console.log(format(99674, { style: "digital" }));

In this example, we’re importing the format function directly from Deno’s standard library hosted in raw format on github.com. When running this script the output should be 00:00:01:39:674:000:000. There we can see, that we could work without a package management system after all.

Caching Mechanism

Similar to tools like yarn or pnpm Deno uses a local cache of all the dependencies it has to download. Deno will download and cache modules the first time you run your script or application. Therefore we need to download them once and are able to use them in every project without needing a redownload.

When Deno encounters a URL import, it follows these steps:

  1. Check if the module is in the local cache.
  2. If not, download the module and store it in the cache.
  3. Use the cached version for subsequent runs.

This caching mechanism ensures that your code runs consistently and doesn’t unnecessarily re-download modules. The cache is stored in a directory that respects your operating system’s conventions:

If you can’t find a directory there, try using deno cache which should give you a similar output like the following:

 deno info
DENO_DIR location: /Users/user/Library/Caches/deno
Remote modules cache: /Users/user/Library/Caches/deno/deps
npm modules cache: /Users/user/Library/Caches/deno/npm
Emitted modules cache: /Users/user/Library/Caches/deno/gen
Language server registries cache: /Users/user/Library/Caches/deno/registries
Origin storage: /Users/user/Library/Caches/deno/location_data

There we can see that my remote modules cache can be found under /Users/user/Library/Caches/deno/deps. If we take a look on the contents of the directory, we see how Deno manages those remote modules:

 ls /Users/user/Library/Caches/deno/deps/https/
cdn.jsdelivr.net          examples.deno.land        jsr.io
cdn.sheetjs.com           fresh.deno.dev            raw.githubusercontent.com
deno.land                 github.com                unpkg.com
esm.sh

Inside of those directories we can find a lot of different files that represent our JavaScript or Typescript files. If we take a look into one of them we can see that it is a 1:1 copy. For example looking into /Users/user/Library/Caches/deno/deps/https/raw.githubusercontent.com/ we might find our duration.ts file used earlier. However the file name is in this case a hash so that Deno can be sure that it knows the file or has to download it.

As you might have read that we have a “npm modules cache” there. We will learn other potential sources of modules in a later chapter. You can be sure, that those will be stored in the cache in the same way.

Security Considerations

While URL imports are powerful, they do raise security concerns. What if the content at a URL changes or becomes malicious? Deno addresses this with several features:

  1. Lockfile: You can generate a lockfile (deno.lock) that records the hashes of your dependencies. Deno will verify these hashes on subsequent runs. You can use deno cache --lock=deno.lock <file> to do this manually.

  2. Integrity Checking: You can specify a hash in your import statement to ensure you’re getting the exact file you expect:

    import { serve } from "https://raw.githubusercontent.com/denoland/std/main/fmt/duration.ts#sha256=abcdef...";
  3. Permissions Model: Deno has a robust permissions model that we’ll cover in more detail later. It prevents scripts from accessing resources (like the network or file system) without explicit permission.

Using npm Modules

Deno isn’t limited to URL-based modules. It can use npm modules directly, bridging the gap between the Deno and Node.js ecosystems. To use an npm module, you use the npm: specifier in front of the module name. For example:

import express from "npm:express";

const app = express();

app.get("/", function (req, res) {
  res.send("Hello World");
});

app.listen(3000);

Running the script in Deno will result in setting up a simple HTTP server with express. This feature allows you to leverage the vast npm ecosystem while enjoying the benefits of Deno’s modern runtime.

If you’re trying this example, you’ll see similar prompts to this:

 deno run main.ts -A
 ⚠️  Deno requests read access to <CWD>.
┠─ Requested by `Deno.cwd()` API.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
 Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >

This is the implementation of Deno’s security model. We have to explicitly give Deno our permission to use different APIs. The message gives you a lot of hints on where to look for more information. We’ll cover the permission model in detail in our Deno on the CLI chapter.

Managing Dependencies

Unlike Node.js with its package.json, Deno doesn’t require a central file listing all dependencies. Instead, dependencies are specified directly in your code through imports. This approach has several advantages:

  1. Clear Dependency Tree: You can easily see which file is using which dependency.
  2. No “Hidden” Dependencies: All dependencies are explicitly imported where they’re used.
  3. Easy to Remove Unused Dependencies: If you remove an import, you’ve removed the dependency.

However, if you want to manage versions of your dependencies centrally, you can use import maps. These allow you to specify versions for your imports in a JSON file, similar to package.json. Here’s an example:

{
  "imports": {
    "colors": "https://raw.githubusercontent.com/denoland/std/main/fmt/colors.ts"
  }
}

With this import map, you can simply use import { bold } from "colors"; in your code, and Deno will use the version specified in the import map.

By default Deno uses a deno.json file with the imports attribute as a default way to manage imported modules.

Tradeoffs and what the HTTP imports might bring into your projects Did Ryan Dahl summarize in a blog post.

📋 Further Documentation

JavaScript’s (Not) Missing Standard Library

One of the long-standing criticisms of JavaScript has been its lack of a comprehensive standard library that doesn’t rely on external dependencies. Deno addresses this issue head-on by providing a well-crafted standard library maintained by its core developers and community contributors.

The Deno Standard Library

Deno’s standard library, referred to as std, is a collection of high-quality, well-tested modules that cover a wide range of functionalities. These modules are designed to work seamlessly with Deno and follow its design principles. A lot of the modules work in NodeJS, Bun or even in the browser.

Key aspects of the Deno standard library:

  1. No External Dependencies: All modules in the standard library are implemented without relying on external packages.

  2. TypeScript First: The entire library is written in TypeScript, providing excellent type checking and autocompletion in supported editors.

  3. Browser APIs: Instead of using custom implementations the library focuses on implementing features with standard Web APIs to ensure compatibility with other runtimes like Bun or Node.

  4. Modular Design: Each module is small and focused, allowing you to import only what you need.

  5. Continuous Improvement: The library is actively maintained and improved by the Deno core team and community contributors.

Example Modules in the Standard Library

Let’s explore a couple of modules in Deno’s standard library:

  1. fs: File system operations

    import { readJson, writeJson } from "https://deno.land/std/fs/mod.ts";
    
    const data = await readJson("config.json");
    await writeJson("output.json", data);
  2. path: Path manipulation utilities

    import { join } from "https://deno.land/std/path/mod.ts";
    
    const fullPath = join("directory", "subdirectory", "file.txt");
  3. http: HTTP server and client utilities

    import { serve } from "https://deno.land/std/http/server.ts";
    
    serve((req) => new Response("Hello, World!"));
  4. datetime: Date and time utilities

    import { format } from "https://deno.land/std/datetime/mod.ts";
    
    console.log(format(new Date(), "yyyy-MM-dd"));
  5. testing: Assertion library for Deno’s built-in test runner

    import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
    
    Deno.test("example test", () => {
      assertEquals(2 + 2, 4);
    });

Benefits of Using the Standard Library

Using Deno’s standard library offers several advantages:

  1. Consistency: The modules are designed to work together seamlessly.
  2. Performance: They’re optimized for use with Deno. (However they work in other runtimes)
  3. Built-in TypeScript Support: No need for separate @types packages.

Versioning and Stability

It’s worth noting that the standard library is versioned independently of Deno itself. When importing from the standard library, you specify a version:

import { serve } from "https://deno.land/std@0.140.0/http/server.ts";

This allows you to lock to a specific version of the standard library, ensuring consistency across different environments and over time.

Since importing standard library modules via the “deno.land” url is deprecated, we will now talk about how we import them correctly.

📋 Further Documentation

JSR - JavaScript Registry

JSR, or JavaScript Registry, is an exciting new project initiated by the Deno team. It aims to provide a modern, TypeScript-first package registry that addresses the limitations of npm while being usable across different JavaScript runtimes.

Key Features of JSR

  1. TypeScript-First: JSR is designed with TypeScript in mind, making it easier to publish and consume TypeScript packages.

  2. Cross-Runtime Compatibility: Packages on JSR can be used not only in Deno, but in Node.js, browsers, and other JavaScript environments.

  3. Integrated Documentation: JSR automatically generates documentation from the published modules, making it easier for users to understand and use the packages.

  4. Transparency: Package contents are visible directly on the JSR website, increasing trust and making it easier to audit dependencies.

  5. Quality Scoring: JSR provides a scoring system that indicates the quality of a package based on factors like documentation, compatibility, and typing.

  6. Dependency Information: Clear visibility of both dependencies and reverse dependencies (packages that depend on a given package).

Using JSR in Deno

Using a package from JSR in your Deno project is straightforward:

import { format } from "jsr:@std/fmt/duration";

console.log(format(99674, { style: "digital" }));

Using the first example we did here, we can substitute the long github.com link with jsr:@std/fmt/duration - Deno does the rest for us. Downloading, caching and providing the duration package for us.

JSR vs npm

While JSR isn’t meant to replace npm, it offers several advantages:

  1. TypeScript-Native: No need for separate @types packages or compilation steps.
  2. Simpler Publishing Process: No need for a separate build step before publishing.
  3. Better Transparency: Easy to see what’s in a package before using it.
  4. Cross-Runtime by Design: Packages are designed to work across different JavaScript environments.

The Future of JSR

As of now, JSR is in its early stages, but it shows great promise. The Deno team plans to make it a community-driven project, with governance that involves the broader JavaScript community. This approach aims to ensure that JSR evolves to meet the needs of the entire JavaScript ecosystem, not only Deno users.

fmt

Deno’s fmt command is your go-to tool for code formatting. It’s inspired by tools like Go’s go fmt command, aiming to provide a standardized and opinionated way to format your code.

Under the hood

Deno uses a Rust-based formatter called dprint. This ensures fast and consistent formatting across your project.

What can it format?

Deno’s formatter supports all the file types you’ll commonly use in a Deno project:

💡 Since Deno 1.46 we have support for HTML, CSS, SCSS, Sass, Less, YAML, Astro, Angular, Svelte and Vue files as well. The formatter for those file types is in an unstable state for now. Update: Since Deno 2.0.0-rc.10 the formatter for those file types is stable. You can find the PR #25753 here. This Deno version also adds support for .vto and .njk files. Which are often used in the lume static generator.

Ignoring formatting

Sometimes you might want to keep a specific piece of code formatted in a certain way. Deno allows for this:

Customizing the formatter

You can tailor the formatter to your project’s needs by adding a fmt section to your deno.json or deno.jsonc file:

{
  "fmt": {
    "useTabs": true,
    "lineWidth": 80,
    "indentWidth": 4,
    "semiColons": true,
    "singleQuote": true,
    "proseWrap": "preserve",
    "include": ["src/"],
    "exclude": ["src/testdata/", "data/fixtures/**/*.ts"]
  }
}

This configuration allows you to:

By setting up your formatting rules, you ensure that your entire team is working with a consistent code style, which can improve readability and reduce merge conflicts.

📋 Further Documentation

lint

Deno comes with its own linter, accessible via the deno lint command. This tool goes beyond formatting your code - it helps ensure consistent coding practices across your project. Think of it as your personal code quality assistant. It behaves similar to popular tools like eslint

What does the Deno linter do?

The Deno linter checks for a variety of potential issues in your code. Examples are:

In total, Deno provides 104 linting rules as of this writing. Out of these, 76 are recommended and enabled by default. This means you get a solid foundation for code quality right out of the box.

Configuring the linter

Like many Deno tools, you can configure the linter in your deno.json or deno.jsonc file. This allows you to set project-wide linting preferences.

Taken from the official documentation is the following example to include or exclude rules or files:\

{
  "lint": {
    "include": ["src/"],
    "exclude": ["src/testdata/", "src/fixtures/**/*.ts"],
    "rules": {
      "tags": ["recommended"],
      "include": ["ban-untagged-todo"],
      "exclude": ["no-unused-vars"]
    }
  }
}

For a comprehensive list of all available lint rules, check out the deno_lint documentation.

Ignoring linter rules

Sometimes, you might need to bypass certain linter rules. Deno makes this easy:

  1. To ignore linting for an entire file:

    // deno-lint-ignore-file
  2. To ignore multiple rules for a file:

    // deno-lint-ignore-file no-explicit-any no-empty
  3. To ignore a rule for a specific line:

    // deno-lint-ignore no-explicit-any
    const myVar: any = getSomeValue();

Here’s a neat feature: you can add comments explaining why you’re ignoring a rule:

// deno-lint-ignore no-explicit-any -- we really need to use 'any' here due to [reason]
const complexData: any = fetchDataFromExternalAPI();

This is great for maintaining code clarity, when working in teams.

📋 Further Documentation

doc

Documentation is crucial for any project, and Deno makes it easy to generate comprehensive documentation for your code.

How it works

Deno’s documentation generator works by reading the JSDoc comments in your code. JSDoc is a markup language used to annotate JavaScript source code files. When you run deno doc, it parses these comments and generates documentation based on them.

Basic usage

To generate documentation for a file, run:

deno doc your_file.ts

This will output the documentation to the console. However, there are more useful ways to use this feature.

Generating HTML documentation

You can create a static HTML website of your documentation:

deno doc --html --name="My Project" main.ts

This command:

By default, this creates a docs folder in your current directory with the generated HTML files.

JSON output

If you prefer machine-readable output, you can generate JSON:

deno doc --json main.ts

This is useful if you want to process the documentation data programmatically.

Linting your documentation

Deno can help ensure your documentation is complete. Use the --lint flag:

deno doc --lint main.ts

This will warn you about missing documentation or inconsistencies.

For example:

error[missing-jsdoc]: exported symbol is missing JSDoc documentation
 --> /Users/user/code/main.ts:8:14
  |
8 | export const myFunc = (str: string) => {
  |              ^


error: Found 1 documentation lint error.

Read more about what the --lint finds in the official documentation

Best practices for documentation

  1. Document all exported symbols (functions, classes, interfaces, etc.).
  2. Provide clear, concise descriptions for each item.
  3. Include examples where appropriate.
  4. Specify types for parameters and return values.

Here’s an example of well-documented code:

/**
 * Represents a user in the system.
 */
export interface User {
  /**
   * The user's full name.
   */
  name: string;

  /**
   * Unique identifier for the user.
   */
  id: number;
}

/**
 * Retrieves the username from a User object.
 * @param {User} user - The user object.
 * @returns {string} The user's name.
 * @example
 * const user = { name: "John Doe", id: 1 };
 * const name = getUsername(user);
 * console.log(name); // Outputs: "John Doe"
 */
export function getUsername(user: User): string {
  return user.name;
}

By following these practices and using Deno’s documentation tools, you can ensure your project is well-documented and easy for others to understand and use.

📋 Further Documentation

test & coverage

Testing is a crucial part of any software development process, and Deno makes it incredibly easy to get started. With Deno’s built-in testing suite, you have no excuse not to test your code!

Writing your first test

Let’s start with a simple example. Say we have a function that adds two numbers (like the function that we get after running deno init):

// main.ts
export function add(a: number, b: number): number {
  return a + b;
}

We can write a test for this function like so:

// main_test.ts
import { assertEquals } from "jsr:@std/assert";
import { add } from "./main.ts";

Deno.test(function addTest() {
  assertEquals(add(2, 3), 5);
});

Here’s what’s happening:

  1. We import the assertEquals function from Deno’s standard library.
  2. We import our add function from the main file.
  3. We use Deno.test to define a test case.
  4. Inside the test, we use assertEquals to check if our function produces the expected result.

Running tests

To run your tests, use the deno test command in your terminal. Deno will automatically find and run all files with names ending in _test.ts or _test.js.

Asynchronous tests

Deno makes it easy to test asynchronous code too. Put an async keyword in front of your function parentheses in your test:

Deno.test("async function test", async () => {
  const result = await someAsyncFunction();
  assertEquals(result, expectedValue);
});

This way we can even test functions that work with Promises such as API calls for example.

More advanced testing

Deno’s testing framework allows for more complex scenarios. You can use t.step() to create subtests within a larger test:

Deno.test("math operations", async (t) => {
  await t.step("addition", () => {
    assertEquals(add(2, 3), 5);
  });

  await t.step("multiplication", () => {
    assertEquals(multiply(2, 3), 6);
  });
});

This structure helps organize related tests and provides more granular test results. For example creating a user in a database before working with this test user.

Test Documentation Written in JSDoc

Deno also supports running tests and type checks inside JSDoc strings. The only thing we need is to put the code to run inside a block of code in the comment. Below is a sample doc string that shows how it’s done.

/**
 * reverse is a function to reverse a string.
 *
 * # Examples
 * ```ts
 * import { assertEquals } from "jsr:@std/assert/equals";
 *
 * const reversed = reverse("hello");
 * assertEquals(reversed, "olleh");
 * ```
 */
export function reverse(s: string): string {
  return s.split("").reverse().join("");
}

If we now run deno test --doc (the --doc is important here), we can see in the output that Deno also executes the examples in our doc strings.

 deno test --doc
Check file:///Users/user/deno/main.ts$42-48.ts
Check file:///Users/user/deno/main.ts$57-63.ts
running 1 test from ./main.ts$42-48.ts
file:///Users/user/deno/main.ts$42-48.ts ... ok (0ms)
running 1 test from ./main.ts$57-63.ts
file:///Users/user/deno/main.ts$57-63.ts ... ok (0ms)

ok | 2 passed | 0 failed (12ms)

This is a cool feature when writing libraries, for example, to show people examples and also have tests written at the same time.

If you want to ignore the example from testing you can use the ignore attribute next to the declaration of the code block language.

/**
 * reverse is a function to reverse a string.
 *
 * # Examples
 * ```ts ignore
 * import { assertEquals } from "jsr:@std/assert/equals";
 *
 * const reversed = reverse("hello");
 * assertEquals(reversed, "olleh");
 * ```
 */
export function reverse(s: string): string {
  return s.split("").reverse().join("");
}

This feature was implemented in PR #25220 - so it’s pretty fresh right in time for Deno 2.0.

Test coverage

Deno provides built-in test coverage analysis. To use it, run your tests with the --coverage flag:

deno test --coverage

This will create a coverage profile. You can then use the deno coverage command to analyze this profile:

deno coverage

This will show you which parts of your code are covered by tests and which aren’t. You can even generate a detailed HTML report of your coverage with the help of lcov and it’s genhtml tool:

deno coverage --lcov --output=cov_profile
genhtml -o cov_profile/html cov_profile/lcov.info

This creates an interactive HTML page showing your code coverage, making it easy to identify areas that need more testing.

📋 Further Documentation

bench

Performance is crucial in many applications, and Deno provides a built-in benchmarking tool to help you measure and optimize your code’s performance.

What is benchmarking?

Benchmarking is the process of measuring the performance of a piece of code. It helps you understand how fast your code runs and can be crucial for identifying bottlenecks or comparing different implementations.

Basic usage

Let’s start with a simple benchmark. Say we have a function we want to measure:

// main.ts
export const add = (a: number, b: number): number {
  return a + b;
}

We can create a benchmark for this function like this:

// bench.ts
import { add } from "./main.ts";

Deno.bench("addition benchmark", () => {
  add(2, 3);
});

To run this benchmark, use the deno bench command:

deno bench bench.ts

Deno will run your benchmark multiple times and provide statistics about its performance.

Understanding the output

When you run a benchmark, you’ll see output in a table similar to this:

cpu: Apple M1
runtime: deno 1.40.3 (aarch64-apple-darwin)

file:///Users/user/project/bench.ts
benchmark      time (avg)        iter/s             (min … max)       p75       p99      p995
-------------------------------------------------- -----------------------------
addition benchmark    4.08 ns/iter   245,136,869.3    (3.99 ns … 9.9 ns)   4.09 ns   4.35 ns  4.4 ns

This output tells you:

When you have multiple files with multiple benchmarks Deno will go through all of them and run them one by one. This might take a moment but will result in output like this:

 deno bench -A
Check file:///Users/user/code/projects/deno-course/code/bench/read_bench.ts
Check file:///Users/user/code/projects/deno-course/code/bench/sort_bench.ts
cpu: Apple M1
runtime: deno 1.46.1 (aarch64-apple-darwin)

file:///Users/user/code/projects/deno-course/code/bench/read_bench.ts
benchmark                           time (avg)        iter/s             (min max)       p75       p99      p995
------------------------------------------------------------------------------------ -----------------------------
readPokedex                          3.73 ms/iter         268.0     (3.61 ms 4.19 ms) 3.76 ms 4.19 ms 4.19 ms
readPokedex with start and end       6.82 µs/iter     146,692.1    (6.25 µs 26.46 µs) 6.75 µs 11 µs 26.46 µs


file:///Users/user/code/projects/deno-course/code/bench/sort_bench.ts
benchmark              time (avg)        iter/s             (min max)       p75       p99      p995
----------------------------------------------------------------------- -----------------------------
Array.sort Arr1        92.29 ns/iter  10,835,411.7   (88.67 ns 105.1 ns) 94.04 ns 102.99 ns 103.9 ns
Array.sort Arr2       100.98 ns/iter   9,902,601.0  (97.12 ns 124.71 ns) 102.72 ns 107.85 ns 110.35 ns
MergeSorting Arr1     854.86 ns/iter   1,169,784.5 (839.41 ns 921.54 ns) 858.71 ns 921.54 ns 921.54 ns
MergeSorting Arr2       1.07 µs/iter     938,260.4     (1.05 µs 1.14 µs) 1.07 µs 1.14 µs 1.14 µs
quickSorting Arr1     569.39 ns/iter   1,756,266.6  (557.1 ns 631.91 ns) 574.1 ns 631.91 ns 631.91 ns
quickSorting Arr2     724.42 ns/iter   1,380,410.7  (710.2 ns 764.66 ns) 729.67 ns 764.66 ns 764.66 ns

More complex benchmarks

You can create more sophisticated benchmarks using additional options:

Deno.bench({
  name: "complex addition benchmark",
  fn: () => {
    add(Math.random() * 1000, Math.random() * 1000);
  },
  baseline: true,
  group: "math operations",
});

This benchmark:

Benchmarking asynchronous code

Similar or even same as the asynchronous test functions we can even benchmark asynchronous functions. This is done in the same way putting the async in front of the parentheses. Regular Javascript syntax.

Deno.bench("async operation", async () => {
  await someAsyncOperation();
});

Controlling benchmark execution

Sometimes, you might want to control what part of your code is being timed. You can do this using b.start() and b.end():

Deno.bench("partial operation", (b) => {
  // Setup code (not timed)
  const data = prepareData();

  b.start();
  // This is the part that will be timed
  processData(data);
  b.end();

  // Cleanup code (not timed)
  cleanupData(data);
});

This allows you to exclude setup and cleanup time from your benchmark.

💡 Note: Calling the benchmarking parameter b is your choice.

If you want to know more about the deno bench command. There is a lot of documentation about this in the deno bench article and in the Deno.bench API docs.

Best practices for benchmarking

  1. Run benchmarks multiple times to account for variability.
  2. Be aware of environmental factors (CPU load, etc.) that might affect results.
  3. Compare benchmarks on the same machine for consistency.
  4. Use realistic data and operations in your benchmarks.

By using Deno’s benchmarking tools, you can gain valuable insights into your code’s performance and make informed optimizations.

📋 Further Documentation

compile

One of my favorite built in tools of Deno is a powerful tool that allows you to create standalone executables from your TypeScript or JavaScript code. This can be incredibly useful for distributing your applications to users who don’t have Deno installed.

Basic usage

To compile a Deno application, use the deno compile command:

deno compile main.ts

This will create an executable file named after your source directory (e.g., foo or foo.exe on Windows). The binary is default for your CPU architecture of your host machine by default.

Cross-compilation

One of the most powerful features of Deno’s compiler is its ability to cross-compile for different platforms. You can create executables for Windows, macOS, or Linux, regardless of your development platform.

To specify a target, use the --target flag:

deno compile --target x86_64-pc-windows-msvc main.ts

This would create a Windows executable, even if you’re developing on macOS or Linux.

Available targets include:

💡 Note: You can find the architecture that Deno is running on when checking the Deno --version command.

Specifying permissions

When compiling, you need to specify all the permissions your application will need at runtime. For example:

deno compile --allow-read --allow-write main.ts

This creates an executable with permissions to read from and write to the file system.

Considerations

While Deno’s compile feature is powerful, there are a few things to keep in mind:

  1. File size: Compiled executables can be large (50MB or more) because they include the Deno runtime.

  2. Performance: There might be a slight startup delay compared to running with deno run, as the executable needs to unpack the bundled code.

  3. Updates: The compiled executable is static. If you update your dependencies, you’ll need to recompile.

  4. Platform specificity: While you can cross-compile, the resulting executable is specific to the target platform and architecture.

Best practices

  1. Minimize dependencies: The more external dependencies your project has, the larger the compiled executable will be.

  2. Test thoroughly: Make sure to test your compiled executable in an environment similar to where it will be deployed.

  3. Version your executables: Keep track of which source code version each executable was compiled from.

  4. Document required permissions: Make sure users know what system access your application requires.

Example: Creating a CLI tool

Let’s say you’ve created a useful command-line tool with Deno. Here’s how you might compile and distribute it:

// cli.ts
import { parse } from "https://deno.land/std/flags/mod.ts";

const { args } = Deno;
const parsedArgs = parse(args);

if (parsedArgs.help) {
  console.log("Usage: cli [options]");
  Deno.exit(0);
}

console.log("Hello from my CLI tool!");
// ... rest of your CLI logic

To compile this:

deno compile cli.ts

📋 Further Documentation

Understanding import.meta

import.meta is a powerful feature in modern JavaScript that provides metadata about the current module. It’s part of the ECMAScript specification and is supported in modern browsers and JavaScript runtimes, including Deno.

What is import.meta?

import.meta is an object that contains contextual information about the module in which it’s accessed. The exact properties available on import.meta can vary depending on the environment, but Deno provides several useful properties.

Key Properties in Deno

  1. import.meta.url: This provides the URL of the current module. In Deno:

    • For local scripts (e.g., deno run main.ts), it’s a file:// URL pointing to the script’s location on the filesystem.
    • For remote scripts, it’s the full URL of the script.
  2. import.meta.main: This boolean property is true if the current module is the main module that was directly run by Deno. It’s used to write code that should execute when a file is run directly, not when it’s imported by another module.

  3. import.meta.resolve(specifier): This method resolves a module specifier relative to the current module.

Practical Uses of import.meta

Let’s look at practical examples of how you might use import.meta in your Deno projects:

  1. Determining if a module is the main module:

    if (import.meta.main) {
      console.log("This code only runs if this module is the main module");
    }

    This is even done in the main.ts file after running deno init.

  2. Getting the directory of the current module:

    import { dirname } from "jsr:@std/path";
    
    const currentDir = dirname(new URL(import.meta.url).pathname);
    console.log(`This module is located in: ${currentDir}`);

import.meta vs __dirname and __filename

If you’re coming from Node.js, you might be used to __dirname and __filename. Deno doesn’t have these globals, but you can achieve the same results with import.meta.url:

import { fromFileUrl } from "jsr:@std/path";

const __filename = fromFileUrl(import.meta.url);
const __dirname = new URL(".", import.meta.url).pathname;

This approach is more explicit about the source of these values and works consistently across different environments.

📋 Further Documentation

Wrapping Up the Deno Toolchain

Concluding this whole chapter of the Deno toolbox you can see that we’re getting a lot out of the box. The whole project is set up in a way that developers can start writing code right away without the need of setting up Typescript, linting, formatting or anything. It all just works.

In the next chapter we will talk about using Deno as a runtime to build a simple CLI tool. We’ll learn about the permission model that makes applications more secure through disabling any permissions (e.g. network, filesystem) by default having to explicitly giving the script our allowance.