Dnt
Deno to npm package build tool.
Install / Use
/learn @denoland/DntREADME
dnt - Deno to Node Transform
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:
- Transforms Deno code to Node including files found by
deno test.- Rewrites module specifiers.
- Injects shims for any
Denonamespace or other global name usages as specified. - Rewrites 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.
- Type checks the output.
- Emits ESM, CommonJS, and TypeScript declaration files along with a package.json file.
- Runs the final output in Node.js through a test runner calling all
Deno.testcalls.
Setup
-
deno add jsr:@deno/dnt -
Create a build script file:
// ex. scripts/build_npm.ts import { build, emptyDir } from "@deno/dnt"; 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"); }, }); -
Ignore the output directory with your source control if you desire (ex. add
npm/to.gitignore). -
Run it and
npm publish:# 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
[dnt] Transforming...
[dnt] Running npm install...
[dnt] Building project...
[dnt] Type checking ESM...
[dnt] Emitting ESM package...
[dnt] Emitting script package...
[dnt] Running tests...
> test
> node test_runner.js
Running tests in ./script/mod.test.js...
test escapeWithinString ... ok
test escapeChar ... ok
Running tests in ./esm/mod.test.js...
test escapeWithinString ... ok
test escapeChar ... ok
[dnt] Complete!
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:
await build({
// ...etc...
typeCheck: false,
test: false,
declaration: false,
scriptModule: false,
});
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":
await build({
// ...etc...
typeCheck: "both",
});
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:
await build({
// ...etc...
filterDiagnostic(diagnostic) {
if (
diagnostic.file?.fileName.endsWith("fmt/colors.ts")
) {
return false; // ignore all diagnostics in this file
}
// etc... more checks here
return true;
},
});
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:
await build({
// ...etc...
scriptModule: false,
});
Shims
dnt will shim the globals specified in the build options. For example, if you specify the following build options:
await build({
// ...etc...
shims: {
deno: true,
},
});
Then write a statement like so...
Deno.readTextFileSync(...);
...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.
import * as dntShim from "./_dnt.shims.js";
dntShim.Deno.readTextFileSync(...);
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:
await build({
// ...etc...
shims: {
deno: "dev",
timers: true,
},
});
Preventing Shimming
To prevent shimming in specific instances, add a // dnt-shim-ignore comment:
// dnt-shim-ignore
Deno.readTextFileSync(...);
...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 theDenonamespace.timers- Shim the globalsetTimeoutandsetIntervalfunctions with Deno and browser compatible versions.prompts- Shim the globalconfirm,alert, andpromptfunctions.blob- Shim theBlobglobal with the one from the"buffer"module.crypto- Shim thecryptoglobal.domException- Shim theDOMExceptionglobal using the "domexception" package (https://www.npmjs.com/package/domexception)undici- Shimfetch,File,FormData,Headers,Request, andResponseby using the "undici" package (https://www.npmjs.com/package/undici).weakRef- Sham for theWeakRefglobal, which usesglobalThis.WeakRefwhen it exists. The sham will throw at runtime when callingderef()andWeakRefdoesn't globally exist, so this is only intended to help type check code that won't actually use it.webSocket- ShimWebSocketby using the ws package.
Deno.test-only shim
If you only want to shim Deno.test then provide the following:
await build({
// ...etc...
shims: {
deno: {
test: "dev",
},
},
});
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:
await build({
scriptModule: false, // node-fetch 3+ only supports ESM
// ...etc...
shims: {
custom: [{
package: {
name: "node-fetch",
version: "~3.1.0",
},
globalNames: [{
// for the `fetch` global...
name: "fetch",
// use the default export of node-fetch
exportName: "default",
}, {
name: "RequestInit",
typeOnly: true, // only used in type declarations
}],
}, {
// this is what `blob: true` does internally
module: "buffer", // uses node's "buffer" module
globalNames: ["Blob"],
}, {
// this is what `domException: true` does internally
package: {
name: "domexception",
version: "^4.0.0",
},
typesPackage: {
name: "@types/domexception",
version: "^4.0.0",
},
globalNames: [{
name: "DOMException",
exportName: "default",
}],
}],
// shims to only use in the tests
customDev: [{
// this is what `timers: "dev"` does internally
package: {
name: "@deno/shim-timers",
version: "~0.1.0",
},
globalNames: ["setTimeout", "setInterval"],
}],
},
});
Local and Remote Shims
Custom shims can also refer to local or remote modules:
await build({
// ...etc...
shims: {
custom: [{
module: "./my-custom-fetch-implementation.ts",
globalNames: ["fetch"],
}, {
module: "https://deno.land/x/some_remote_shim_module/mod.ts",
globalNames: ["setTimeout"],
}],
},
});
Where my-custom-fetch-implementation.ts contains:
export function fetch(/* etc... */) {
// etc...
}
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:
await build({
// ...etc...
mappings: {
"https://deno.land/x/code_block_writer@11.0.0/mod.ts": {
name: "code-block-writer",
version: "^11.0.0",
// optionally specify if this should be a peer dependency
peerDependency: false,
},
},
});
This will:
- Change all
"https://deno.land/x/code_block_writer@11.0.0/mod.ts"specifiers to"code-block-writer" - 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 `su
