Deno to npm package build tool.
Deno to npm package build tool.
Takes a Deno module and creates an npm package for use in Node.js.
There are several steps done in a pipeline:
deno test.Deno
namespace or other global name usages as specified.Deno.test calls.deno add jsr:@deno/dnt
Create a build script file:
…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[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.cjs
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!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,
});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",
});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 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,
});Specify a module to load before running the tests with the testPreloadModule
build option:
await build({
// ...etc...
testPreloadModule: "./scripts/test_preload.ts",
});This is useful for setting up the Node.js environment the tests run in without affecting the distributed code. For example, when the distributed code assumes a global exists that Node.js doesn't have:
// scripts/test_preload.ts
import { Headers, Response } from "npm:undici";
Object.assign(globalThis, { Headers, Response });The module is transformed and type checked like the test files are and it is not included in the npm package. It is loaded once for each of the emitted script and ESM output, before any test file.
All the test files run in the same process by default, so the module state of a
test file can affect the next one. For example, the global hooks of
@std/testing/bdd error with "Cannot add global hooks after a global test is
registered" when a second test file uses them.
Set the testIsolation build option to "process" to run each test file in its
own process, which is what deno test does by running each file in its own
isolate:
await build({
// ...etc...
testIsolation: "process",
});This is slower because it starts a process per test file, and a preload module is loaded before each test file rather than once per output.
dnt adds polyfills for language features that may not exist in the environment
implied by compilerOptions.target. If you're targeting a runtime that already
supports a feature, use the polyfills build option to opt out and keep the
extra code out of your package:
await build({
// ...etc...
polyfills: {
importMeta: false,
},
});Pass true or false instead of an object to enable or disable every polyfill
at once. Anything you don't specify continues to be decided by the target, so
polyfills overrides the target rather than replacing it.
The supported names are arrayFindLast, arrayFromAsync, errorCause,
importMeta, objectHasOwn, promiseWithResolvers, and stringReplaceAll.
Note that the polyfills also provide the type declarations for the features they
polyfill, so opting out of one means relying on your compilerOptions.lib
declarations for it instead.
The importMeta polyfill can only be disabled when the scriptModule build
option is false, because import.meta is not valid CommonJS:
await build({
// ...etc...
scriptModule: false,
polyfills: {
importMeta: false,
},
});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(...);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 crypto
shim in the distributed code, you would do:
await build({
// ...etc...
shims: {
deno: "dev",
crypto: true,
},
});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.
Set any of these properties to true (distribution and test) or "dev" (test
only) to use them.
deno - Shim the Deno namespace.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 shimIf 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
In addition to the pre-defined shim options, you may specify your own custom packages to use to shim globals.
For example:
…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.
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/[email protected]/mod.ts": {
name: "code-block-writer",
version: "^11.0.0",
// optionally specify if this should be a peer dependency
peerDependency: false,
},
},
});This will:
"https://deno.land/x/[email protected]/mod.ts" specifiers
to "code-block-writer""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.
A bare specifier may be used, in which case it's resolved via the config file's import map. For example, given a deno.json with:
{
"imports": {
"code-block-writer": "https://deno.land/x/[email protected]/mod.ts"
}
}...the specifier can be mapped by the name used in the code:
await build({
// ...etc...
mappings: {
"code-block-writer": {
name: "code-block-writer",
version: "^11.0.0",
},
},
});A key that has a scheme or that looks like a relative or absolute path is not treated as a bare specifier.
A jsr: specifier may be mapped the same way:
await build({
// ...etc...
mappings: {
"jsr:@scope/name": {
name: "scope-name",
},
},
});The version requirement is ignored when matching, so this maps
import ... from "jsr:@scope/name@^1.0.0" as well as an import of a bare
specifier that an import map points at jsr:@scope/name@^1.0.0. That version
requirement is used for the dependency, but specifying a version on the
mapping will override it.
Sub paths are matched exactly though, so jsr:@scope/name/sub requires its own
mapping.
Say an npm package called example had a subpath at sub_path.js and you
wanted to map https://deno.land/x/[email protected]/sub_path.ts to that subpath.
To specify this, you would do the following:
await build({
// ...etc...
mappings: {
"https://deno.land/x/[email protected]/sub_path.ts": {
name: "example",
version: "^0.1.0",
subPath: "sub_path.js", // note this
},
},
});This would cause the following:
import * as mod from "https://deno.land/x/[email protected]/sub_path.ts";No open issues yet, or sync has not completed.