dnt

Takes a Deno module and creates an npm package for use in Node.js

README

dnt - Deno to Node Transform


deno doc

Deno to npm package build tool.

What does this do?


Takes a Deno module and creates an npm package for use in Node.js.

There are several steps done in a pipeline:

1. Transforms Deno code to Node/canonical TypeScript including files found by
   deno test.
   - Rewrites module specifiers.
   - Injects shims for anyDeno
     namespace or other global name usages as specified.
   - Rewrites Skypack and esm.sh
     specifiers to bare specifiers and includes these dependencies in a
     package.json.
   - When remote modules cannot be resolved to an npm package, it downloads them
     and rewrites specifiers to make them local.
   - Allows mapping any specifier to an npm package.
1. Type checks the output.
1. Emits ESM, CommonJS, and TypeScript declaration files along with a
   _package.json_ file.
1. Runs the final output in Node.js through a test runner calling all
   Deno.test calls.

Setup


1. Create a build script file:

   ts
   // ex. scripts/build_npm.ts
   import { build, emptyDir } from "https://deno.land/x/dnt/mod.ts";

   await emptyDir("./npm");

   await build({
     entryPoints: ["./mod.ts"],
     outDir: "./npm",
     shims: {
       // see JS docs for overview and more options
       deno: true,
     },
     package: {
       // package.json properties
       name: "your-package",
       version: Deno.args[0],
       description: "Your package.",
       license: "MIT",
       repository: {
         type: "git",
         url: "git+https://github.com/username/repo.git",
       },
       bugs: {
         url: "https://github.com/username/repo/issues",
       },
     },
     postBuild() {
       // steps to run after building and before running the tests
       Deno.copyFileSync("LICENSE", "npm/LICENSE");
       Deno.copyFileSync("README.md", "npm/README.md");
     },
   });
  

1. Ignore the output directory with your source control if you desire (ex. add
   npm/ to .gitignore).

1. Run it and npm publish:

   bash
   # run script
   deno run -A scripts/build_npm.ts 0.1.0

   # go to output directory and publish
   cd npm
   npm publish
  

Example Build Logs


  1. ```
  2. [dnt] Transforming...
  3. [dnt] Running npm install...
  4. [dnt] Building project...
  5. [dnt] Type checking ESM...
  6. [dnt] Emitting ESM package...
  7. [dnt] Emitting script package...
  8. [dnt] Running tests...

  9. > test
  10. > node test_runner.js

  11. Running tests in ./script/mod.test.js...

  12. test escapeWithinString ... ok
  13. test escapeChar ... ok

  14. Running tests in ./esm/mod.test.js...

  15. test escapeWithinString ... ok
  16. test escapeChar ... ok
  17. [dnt] Complete!
  18. ```

Docs


Disabling Type Checking, Testing, Declaration Emit, or CommonJS/UMD Output


Use the following options to disable any one of these, which are enabled by
default:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   typeCheck: false,
  5.   test: false,
  6.   declaration: false,
  7.   scriptModule: false,
  8. });
  9. ```

Type Checking Both ESM and Script Output


By default, only the ESM output will be type checked for performance reasons.
That said, it's recommended to type check both the ESM and the script (CJS/UMD)
output by setting typeCheck to "both":

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   typeCheck: "both",
  5. });
  6. ```

Ignoring Specific Type Checking Errors


Sometimes you may be getting a TypeScript error that is not helpful and you want
to ignore it. This is possible by using the filterDiagnostic option:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   filterDiagnostic(diagnostic) {
  5.     if (
  6.       diagnostic.file?.fileName.endsWith("fmt/colors.ts")
  7.     ) {
  8.       return false; // ignore all diagnostics in this file
  9.     }
  10.     // etc... more checks here
  11.     return true;
  12.   },
  13. });
  14. ```

This is especially useful for ignoring type checking errors in remote
dependencies.

Top Level Await


Top level await doesn't work in CommonJS/UMD and dnt will error if a top level
await is used and you are outputting CommonJS/UMD code. If you want to output a
CommonJS/UMD package then you'll have to restructure your code to not use any
top level awaits. Otherwise, set the scriptModule build option to false:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   scriptModule: false,
  5. });
  6. ```

Shims


dnt will shim the globals specified in the build options. For example, if you
specify the following build options:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   shims: {
  5.     deno: true,
  6.   },
  7. });
  8. ```

Then write a statement like so...

  1. ```ts
  2. Deno.readTextFileSync(...);
  3. ```

...dnt will create a shim file in the output, re-exporting the
@deno/shim-deno npm shim package
and change the Deno global to be used as a property of this object.

  1. ```ts
  2. import * as dntShim from "./_dnt.shims.js";

  3. dntShim.Deno.readTextFileSync(...);
  4. ```

Test-Only Shimming


If you want a shim to only be used in your test code as a dev dependency, then
specify "dev" for the option.

For example, to use the Deno namespace only for development and the
setTimeout and setInterval browser/Deno compatible shims in the distributed
code, you would do:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   shims: {
  5.     deno: "dev",
  6.     timers: true,
  7.   },
  8. });
  9. ```

Preventing Shimming


To prevent shimming in specific instances, add a // dnt-shim-ignore comment:

  1. ```ts
  2. // dnt-shim-ignore
  3. Deno.readTextFileSync(...);
  4. ```

...which will now output that code as-is.

Built-In Shims


Set any of these properties to true (distribution and test) or "dev" (test
only) to use them.

- deno - Shim the Deno namespace.
- timers - Shim the global setTimeout and setInterval functions with Deno
  and browser compatible versions.
- prompts - Shim the global confirm, alert, and prompt functions.
- blob - Shim the Blob global with the one from the "buffer" module.
- crypto - Shim the crypto global.
- domException - Shim the DOMException global using the "domexception"
  package (https://www.npmjs.com/package/domexception)
- undici - Shim fetch, File, FormData, Headers, Request, and
  Response by using the "undici" package
  (https://www.npmjs.com/package/undici).
- weakRef - Sham for the WeakRef global, which uses globalThis.WeakRef
  when it exists. The sham will throw at runtime when calling deref() and
  WeakRef doesn't globally exist, so this is only intended to help type check
  code that won't actually use it.
- webSocket - Shim WebSocket by using the
  ws package.

Deno.test-only shim

If you only want to shim Deno.test then provide the following:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   shims: {
  5.     deno: {
  6.       test: "dev",
  7.     },
  8.   },
  9. });
  10. ```

This may be useful in Node v14 and below where the full deno shim doesn't always
work. See the section on Node v14 below for more details

Custom Shims (Advanced)


In addition to the pre-defined shim options, you may specify your own custom
packages to use to shim globals.

For example:

  1. ```ts
  2. await build({
  3.   scriptModule: false, // node-fetch 3+ only supports ESM
  4.   // ...etc...
  5.   shims: {
  6.     custom: [{
  7.       package: {
  8.         name: "node-fetch",
  9.         version: "~3.1.0",
  10.       },
  11.       globalNames: [{
  12.         // for the `fetch` global...
  13.         name: "fetch",
  14.         // use the default export of node-fetch
  15.         exportName: "default",
  16.       }, {
  17.         name: "RequestInit",
  18.         typeOnly: true, // only used in type declarations
  19.       }],
  20.     }, {
  21.       // this is what `blob: true` does internally
  22.       module: "buffer", // uses node's "buffer" module
  23.       globalNames: ["Blob"],
  24.     }, {
  25.       // this is what `domException: true` does internally
  26.       package: {
  27.         name: "domexception",
  28.         version: "^4.0.0",
  29.       },
  30.       typesPackage: {
  31.         name: "@types/domexception",
  32.         version: "^4.0.0",
  33.       },
  34.       globalNames: [{
  35.         name: "DOMException",
  36.         exportName: "default",
  37.       }],
  38.     }],
  39.     // shims to only use in the tests
  40.     customDev: [{
  41.       // this is what `timers: "dev"` does internally
  42.       package: {
  43.         name: "@deno/shim-timers",
  44.         version: "~0.1.0",
  45.       },
  46.       globalNames: ["setTimeout", "setInterval"],
  47.     }],
  48.   },
  49. });
  50. ```

Local and Remote Shims


Custom shims can also refer to local or remote modules:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   shims: {
  5.     custom: [{
  6.       module: "./my-custom-fetch-implementation.ts",
  7.       globalNames: ["fetch"],
  8.     }, {
  9.       module: "https://deno.land/x/some_remote_shim_module/mod.ts",
  10.       globalNames: ["setTimeout"],
  11.     }],
  12.   },
  13. });
  14. ```

Where my-custom-fetch-implementation.ts contains:

  1. ```ts
  2. export function fetch(/* etc... */) {
  3.   // etc...
  4. }
  5. ```

This is useful in situations where you want to implement your own shim.

Specifier to npm Package Mappings


In most cases, dnt won't know about an npm package being available for one of
your dependencies and will download remote modules to include in your package.
There are scenarios though where an npm package may exist and you want to use it
instead. This can be done by providing a specifier to npm package mapping.

For example:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   mappings: {
  5.     "https://deno.land/x/code_block_writer@11.0.0/mod.ts": {
  6.       name: "code-block-writer",
  7.       version: "^11.0.0",
  8.       // optionally specify if this should be a peer dependency
  9.       peerDependency: false,
  10.     },
  11.   },
  12. });
  13. ```

This will:

1. Change all "https://deno.land/x/code_block_writer@11.0.0/mod.ts" specifiers
   to "code-block-writer"
2. Add a package.json dependency for "code-block-writer": "^11.0.0".

Note that dnt will error if you specify a mapping and it is not found in the
code. This is done to prevent the scenario where a remote specifier's version is
bumped and the mapping isn't updated.

Mapping specifier to npm package subpath


Say an npm package called example had a subpath at sub_path.js and you
wanted to map https://deno.land/x/example@0.1.0/sub_path.ts to that subpath.
To specify this, you would do the following:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   mappings: {
  5.     "https://deno.land/x/example@0.1.0/sub_path.ts": {
  6.       name: "example",
  7.       version: "^0.1.0",
  8.       subPath: "sub_path.js", // note this
  9.     },
  10.   },
  11. });
  12. ```

This would cause the following:

  1. ```ts
  2. import * as mod from "https://deno.land/x/example@0.1.0/sub_path.ts";
  3. ```

...to go to...

  1. ```ts
  2. import * as mod from "example/sub_path.js";
  3. ```

...with a dependency on "example": "^0.1.0".

Multiple Entry Points


To do this, specify multiple entry points like so (ex. an entry point at . and
another at ./internal):

  1. ```ts
  2. await build({
  3.   entryPoints: ["mod.ts", {
  4.     name: "./internal",
  5.     path: "internal.ts",
  6.   }],
  7.   // ...etc...
  8. });
  9. ```

This will create a package.json with these as exports:

  1. ```jsonc
  2. {
  3.   "name": "your-package",
  4.   // etc...
  5.   "main": "./script/mod.js",
  6.   "module": "./esm/mod.js",
  7.   "types": "./types/mod.d.ts",
  8.   "exports": {
  9.     ".": {
  10.       "import": {
  11.         "types": "./types/mod.d.ts",
  12.         "default": "./esm/mod.js"
  13.       },
  14.       "require": {
  15.         "types": "./types/mod.d.ts",
  16.         "default": "./script/mod.js"
  17.       }
  18.     },
  19.     "./internal": {
  20.       "import": {
  21.         "types": "./types/internal.d.ts",
  22.         "default": "./esm/internal.js"
  23.       },
  24.       "require": {
  25.         "types": "./types/internal.d.ts",
  26.         "default": "./script/internal.js"
  27.       }
  28.     }
  29.   }
  30. }
  31. ```

Now these entry points could be imported like
import * as main from "your-package" and
import * as internal from "your-package/internal";.

Bin/CLI Packages


To publish an npm
similar to deno install, add a kind: "bin" entry point:

  1. ```ts
  2. await build({
  3.   entryPoints: [{
  4.     kind: "bin",
  5.     name: "my_binary", // command name
  6.     path: "./cli.ts",
  7.   }],
  8.   // ...etc...
  9. });
  10. ```

This will add a "bin" entry to the package.json and add #!/usr/bin/env node
to the top of the specified entry point.

Node and Deno Specific Code


You may find yourself in a scenario where you want to run certain code based on
whether someone is in Deno or if someone is in Node and feature testing is not
possible. For example, say you want to run the deno executable when the code
is running in Deno and the node executable when it's running in Node.

which_runtime


One option to handle this, is to use the
[which_runtime](https://deno.land/x/which_runtime) deno.land/x module which
provides some exports saying if the code is running in Deno or Node.

Node and Deno Specific Modules


Another option is to create node and deno specific modules. This can be done by
specifying a mapping to a module:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   mappings: {
  5.     "./file.deno.ts": "./file.node.ts",
  6.   },
  7. });
  8. ```

Then within the file, use // dnt-shim-ignore directives to disable shimming if
you desire.

A mapped module should be written similar to how you write Deno code (ex. use
extensions on imports), except you can also import built-in node modules such as
import fs from "fs"; (just remember to include an @types/node dev dependency
under the package.devDependencies object when calling the build function, if
necessary).

Pre & Post Build Steps


Since the file you're calling is a script, simply add statements before and
after the await build({ ... }) statement:

  1. ```ts
  2. import { build, emptyDir } from "https://deno.land/x/dnt/mod.ts";

  3. // run pre-build steps here
  4. await emptyDir("./npm");

  5. // build
  6. await build({
  7.   // ...etc..
  8. });

  9. // run post-build steps here
  10. await Deno.copyFile("LICENSE", "npm/LICENSE");
  11. await Deno.copyFile("README.md", "npm/README.md");
  12. ```

Including Test Data Files


Your Deno tests might rely on test data files. One way of handling this is to
copy these files to be in the output directory at the same relative path your
Deno tests run with.

For example:

  1. ```ts
  2. import { copy } from "https://deno.land/std@x.x.x/fs/mod.ts";

  3. await Deno.remove("npm", { recursive: true }).catch((_) => {});
  4. await copy("testdata", "npm/esm/testdata", { overwrite: true });
  5. await copy("testdata", "npm/script/testdata", { overwrite: true });

  6. await build({
  7.   // ...etc...
  8. });

  9. // ensure the test data is ignored in the `.npmignore` file
  10. // so it doesn't get published with your npm package
  11. await Deno.writeTextFile(
  12.   "npm/.npmignore",
  13.   "esm/testdata/\nscript/testdata/\n",
  14.   { append: true },
  15. );
  16. ```

Alternatively, you could also use the
[which_runtime](https://deno.land/x/which_runtime) module and use a different
directory path when the tests are running in Node. This is probably more ideal
if you have a lot of test data.

Test File Matching


By default, dnt uses the same search pattern
that deno test uses to find test files. To override this, provide a
testPattern and/or rootTestDir option:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   testPattern: "**/*.test.{ts,tsx,js,mjs,jsx}",
  5.   // and/or provide a directory to start searching for test
  6.   // files from, which defaults to the current working directory
  7.   rootTestDir: "./tests",
  8. });
  9. ```

Import Map / deno.json Support


To use an import map or deno.json file with "imports" and/or "scopes", add
an importMap entry to your build object:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   importMap: "deno.json",
  5. });
  6. ```

Note there is no support for the deno.json importMap key. Either embed that in
your deno.json or specify the import map in this property directly. Also note
that the deno.json is not auto-discovered—you must explicitly specify it.

GitHub Actions - Npm Publish on Tag


1. Ensure your build script accepts a version as a CLI argument and sets that in
   the package.json object. For example:

   ts
   await build({
     // ...etc...
     package: {
       version: Deno.args[0],
       // ...etc...
     },
   });
  

   Note: You may wish to remove the leading v in the tag name if it exists
   (ex. Deno.args[0]?.replace(/^v/, ""))

1. In your npm settings, create an _automation_ access token (see

1. In your GitHub repo or organization, add a secret for NPM_TOKEN with the
   value created in the previous step (see

1. In your GitHub Actions workflow, get the tag name, setup node, run your build
   script, then publish to npm.

   yml
   # ...setup deno and run deno test here as you normally would...

   - name: Get tag version
     if: startsWith(github.ref, 'refs/tags/')
     id: get_tag_version
     run: echo TAG_VERSION=${GITHUB_REF/refs\/tags\//} >> $GITHUB_OUTPUT
   - uses: actions/setup-node@v3
     with:
       node-version: '18.x'
       registry-url: 'https://registry.npmjs.org'
   - name: npm build
     run: deno run -A ./scripts/build_npm.ts ${{steps.get_tag_version.outputs.TAG_VERSION}}
   - name: npm publish
     if: startsWith(github.ref, 'refs/tags/')
     env:
       NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
     run: cd npm && npm publish
  

   Note that the build script always runs even when not publishing. This is to
   ensure your build and tests pass on each commit.

1. Ensure the workflow will run on tag creation. For example, see

Using Another Package Manager


You may want to use another Node.js package manager instead of npm, such as Yarn
or pnpm. To do this, override the packageManager option in the build options.

For example:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   packageManager: "yarn", // or "pnpm"
  5. });
  6. ```

You can even specify an absolute path to the executable file of the package
manager:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   packageManager: "/usr/bin/pnpm",
  5. });
  6. ```

DOM Types


If you wish to compile with DOM types for type checking, you may specify a "dom"
lib compiler option when building:

  1. ```ts
  2. await build({
  3.   // ...etc...
  4.   compilerOptions: {
  5.     lib: ["ES2021", "DOM"],
  6.   },
  7. });
  8. ```

Node v14 and Below


dnt should be able to target old versions of Node by specifying a
{ compilerOption: { target: ... }} value in the build options (see
for what target maps to what Node version). A problem though is that certain
shims might not work in old versions of Node.

If wanting to target a version of Node v14 and below, its recommend to use the
Deno.test-only shim (described above) and then making use of the "mappings"
feature to write Node-only files where you can handle differences.
Alternatively, see if changes to the shim libraries might make it run on old
versions of Node. Unfortunately, certain features are impossible or infeasible
to get working.

node_deno_shims for more details.

JS API Example


For only the Deno to canonical TypeScript transform which may be useful for
bundlers, use the following:

  1. ```ts
  2. // docs: https://doc.deno.land/https/deno.land/x/dnt/transform.ts
  3. import { transform } from "https://deno.land/x/dnt/transform.ts";

  4. const outputResult = await transform({
  5.   entryPoints: ["./mod.ts"],
  6.   testEntryPoints: ["./mod.test.ts"],
  7.   shims: [],
  8.   testShims: [],
  9.   // mappings: {}, // optional specifier mappings
  10. });
  11. ```

Rust API Example


  1. ```rust
  2. use std::path::PathBuf;

  3. use deno_node_transform::ModuleSpecifier;
  4. use deno_node_transform::transform;
  5. use deno_node_transform::TransformOptions;

  6. let output_result = transform(TransformOptions {
  7.   entry_points: vec![ModuleSpecifier::from_file_path(PathBuf::from("./mod.ts")).unwrap()],
  8.   test_entry_points: vec![ModuleSpecifier::from_file_path(PathBuf::from("./mod.test.ts")).unwrap()],
  9.   shims: vec![],
  10.   test_shims: vec![],
  11.   loader: None, // use the default loader
  12.   specifier_mappings: None,
  13. }).await?;
  14. ```