Documentation tests

Ende can evaluate the code snippets written in your JSDoc comments and markdown files and run them as tests. This keeps the examples in your documentation honest: when an API changes, its outdated examples fail in CI instead of misleading readers.

Example code blocks

example.ts
/**
 * # Examples
 *
 * ```ts
 * import { assertEquals } from "jsr:@ende/assert/equals";
 *
 * const sum = add(1, 2);
 * assertEquals(sum, 3);
 * ```
 */
export function add(a: number, b: number): number {
  return a + b;
}

The triple backticks mark the start and end of code blocks, the language is determined by the language identifier attribute which may be one of the following:

  • js
  • javascript
  • mjs
  • cjs
  • jsx
  • ts
  • typescript
  • mts
  • cts
  • tsx

If no language identifier is specified then the language is inferred from media type of the source document that the code block is extracted from.

ende test --doc example.ts

The above command will extract this example, turn it into a pseudo test case that looks like below:

example.ts$4-10.ts
import { assertEquals } from "jsr:@ende/assert/equals";
import { add } from "file:///path/to/example.ts";

Ende.test("example.ts$4-10.ts", async () => {
  const sum = add(1, 2);
  assertEquals(sum, 3);
});

and then run it as a standalone module living in the same directory as the module being documented.

Want to type-check only?

If you want to type-check your code snippets in JSDoc and markdown files without actually running them, you can use ende check command with --doc option (for JSDoc) or with --doc-only option (for markdown) instead.

Exported items are automatically imported

Looking at the generated test code above, you will notice that it includes the import statement to import the add function even though the original code block does not have it. When documenting a module, any items exported from the module are automatically included in the generated test code using the same name.

Let's say we have the following module:

example.ts
/**
 * # Examples
 *
 * ```ts
 * import { assertEquals } from "jsr:@ende/assert/equals";
 *
 * const sum = add(ONE, getTwo());
 * assertEquals(sum, 3);
 * ```
 */
export function add(a: number, b: number): number {
  return a + b;
}

export const ONE = 1;
export default function getTwo() {
  return 2;
}

This will get converted to the following test case:

example.ts$4-10.ts
import { assertEquals } from "jsr:@ende/assert/equals";
import { add, ONE }, getTwo from "file:///path/to/example.ts";

Ende.test("example.ts$4-10.ts", async () => {
  const sum = add(ONE, getTwo());
  assertEquals(sum, 3);
});

Hashbang and narrowed permissions

If a code example starts with a hashbang, it is validated against supported Ende cli flags, and permissions flags are forwarded to the generated Ende.test.

/**
 * Prints the value of an environment variable.
 *
 * ```ts
 * #!/usr/bin/env -S ende run --allow-env=MY_ENV_VAR
 * console.log(Ende.env.get("MY_ENV_VAR"));
 * ```
 */

Below is how each permission flag is interpreted:

Flag Generated permissions
--allow-all Inherits all permissions from ende test
--allow-* Inherits the specified permission from ende test
--allow-*=… Restrict the specified permission to provided values
--deny-* Explicitly revoke the specified permission
--deny-*=… Currently unsupported (results in a test failure)
--permission-set Currently unsupported (ignored)
--ignore-* Currently unsupported (ignored)
No permission flags None

Note

A code example never runs with broader permissions than what ende test was granted, even if the hashbang itself specifies broader ones.

Skipping code blocks

You can skip the evaluation of code blocks by adding the ignore attribute.

/**
 * This code block will not be run.
 *
 * ```ts ignore
 * await sendEmail("[email protected]");
 * ```
 */
export async function sendEmail(to: string) {
  // send an email to the given address...
}