Get Started with the Dagger Node.js SDK
Introduction
This tutorial teaches you the basics of using Dagger in Node.js. You will learn how to:
- Install the Node.js SDK
- Create a Node.js CI tool that tests and builds a Node.js application for multiple Node.js versions using the Node.js SDK
Requirements
This tutorial assumes that:
- You have a Node.js development environment with Node.js 16.x or later. If not, install NodeJS.
- You have a Node.js application developed in either JavaScript or TypeScript. If not, follow the steps in Appendix A to create an example React application in TypeScript.
- You have the Dagger CLI installed on the host system. If not, install the Dagger CLI.
- You have Docker installed and running on the host system. If not, install Docker.
Step 1: Install the Dagger Node.js SDK
The Dagger Node.js SDK requires NodeJS 16.x or later.
Install the Dagger Node.js SDK in your project using npm
or yarn
:
- npm
- yarn
npm install @dagger.io/dagger@latest --save-dev
yarn add @dagger.io/dagger --dev
Step 2: Create a Dagger client in Node.js
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
Install the TypeScript engine (if not already present):
npm install ts-node typescript
Create a TypeScript configuration file (if not already present):
npx tsc --init --module esnext --moduleResolution nodenext
In your project directory, create a new file named build.mts
and add the following code to it.
import { connect, Client } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client: Client) => {
// get Node image
// get Node version
const node = client.container().from("node:16").withExec(["node", "-v"])
// execute
const version = await node.stdout()
// print output
console.log("Hello from Dagger and Node " + version)
},
{ LogOutput: process.stderr },
)
In your project directory, create a new file named build.mjs
and add the following code to it.
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client) => {
// get Node image
// get Node version
const node = client.container().from("node:16").withExec(["node", "-v"])
// execute
const version = await node.stdout()
// print output
console.log("Hello from Dagger and Node " + version)
},
{ LogOutput: process.stderr },
)
In your project directory, create a new file named build.js
and add the following code to it.
;(async function () {
// initialize Dagger client
let connect = (await import("@dagger.io/dagger")).connect
connect(
async (client) => {
// get Node image
// get Node version
const node = client.container().from("node:16").withExec(["node", "-v"])
// execute
const version = await node.stdout()
// print output
console.log("Hello from Dagger and Node " + version)
},
{ LogOutput: process.stderr },
)
})()
This Node.js stub imports the Dagger SDK and defines an asynchronous function. This function performs the following operations:
- It creates a Dagger client with
connect()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
container().from()
method to initialize a new container from a base image. In this example, the base image is thenode:16
image. This method returns aContainer
representing an OCI-compatible container image. - It uses the
Container.withExec()
method to define the command to be executed in the container - in this case, the commandnode -v
, which returns the Node version string. ThewithExec()
method returns a revisedContainer
with the results of command execution. - It retrieves the output stream of the last executed with the
Container.stdout()
method and prints the result to the console.
Run the Node.js CI tool by executing the command below from the project directory:
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
dagger run node --loader ts-node/esm ./build.mts
dagger run node ./build.mjs
dagger run node ./build.js
The dagger run
command executes the specified command in a Dagger session and displays live progress. At the end of the process, the tool outputs a string similar to the one below.
Hello from Dagger and Node v16.18.1
Step 3: Test against a single Node.js version
Now that the basic structure of the CI tool is defined and functional, the next step is to flesh it out to actually test and build the application.
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
Replace the build.mts
file from the previous step with the version below (highlighted lines indicate changes):
import { connect, Client } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client: Client) => {
// get reference to the local project
const source = client.host().directory(".", { exclude: ["node_modules/"] })
// get Node image
const node = client.container().from("node:16")
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export("./build")
},
{ LogOutput: process.stderr },
)
Replace the build.mjs
file from the previous step with the version below (highlighted lines indicate changes):
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client) => {
// get reference to the local project
const source = client.host().directory(".", { exclude: ["node_modules/"] })
// get Node image
const node = client.container().from("node:16")
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export("./build")
},
{ LogOutput: process.stderr },
)
Replace the build.js
file from the previous step with the version below (highlighted lines indicate changes):
;(async function () {
// initialize Dagger client
let connect = (await import("@dagger.io/dagger")).connect
connect(
async (client) => {
// get reference to the local project
const source = client
.host()
.directory(".", { exclude: ["node_modules/"] })
// get Node image
const node = client.container().from("node:16")
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export("./build")
},
{ LogOutput: process.stderr },
)
})()
The revised code now does the following:
- It creates a Dagger client with
connect()
as before. - It uses the client's
host().directory(".", ["node_modules/"])
method to obtain a reference to the current directory on the host. This reference is stored in thesource
variable. It also will ignore thenode_modules
directory on the host since we passed that in as an excluded directory. - It uses the client's
container().from()
method to initialize a new container from a base image. This base image is the Node.js version to be tested against - thenode:16
image. This method returns a newContainer
object with the results. - It uses the
Container.withDirectory()
method to write the host directory into the container at the/src
path, and theContainer.withWorkdir()
method to set the working directory in the container to that path. The revisedContainer
is stored in therunner
constant. - It uses the
Container.withExec()
method to define the command to run tests in the container - in this case, the commandnpm test -- --watchAll=false
. - It uses the
Container.sync()
method to execute the command. - It invokes the
Container.withExec()
method again, this time to define the build commandnpm run build
in the container. - It obtains a reference to the
build/
directory in the container with theContainer.directory()
method. This method returns aDirectory
object. - It writes the
build/
directory from the container to the host using theDirectory.export()
method.
The from()
, withDirectory()
, withWorkdir()
and withExec()
methods all return a Container
, making it easy to chain method calls together and create a pipeline that is intuitive to understand.
Run the Node.js CI tool by executing the command below:
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
dagger run node --loader ts-node/esm ./build.mts
dagger run node ./build.mjs
dagger run node ./build.js
The tool tests and builds the application, logging the output of the test and build operations to the console as it works. At the end of the process, the built application is available in a new build
folder in the project directory. Here is an example of the output when building a React application:
tree build
build
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
├── css
│ ├── main.073c9b0a.css
│ └── main.073c9b0a.css.map
├── js
│ ├── 787.28cb0dcd.chunk.js
│ ├── 787.28cb0dcd.chunk.js.map
│ ├── main.f5e707f0.js
│ ├── main.f5e707f0.js.LICENSE.txt
│ └── main.f5e707f0.js.map
└── media
└── logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
Step 4: Test against multiple Node.js versions
Now that the Node.js CI tool can test the application against a single Node.js version, the next step is to extend it for multiple Node.js versions.
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
Replace the build.mts
file from the previous step with the version below (highlighted lines indicate changes):
import { connect, Client } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client: Client) => {
// Set Node versions against which to test and build
const nodeVersions = ["12", "14", "16"]
// get reference to the local project
const source = client.host().directory(".", { exclude: ["node_modules/"] })
// for each Node version
for (const nodeVersion of nodeVersions) {
// get Node image
const node = client.container().from(`node:${nodeVersion}`)
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application using specified Node version
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export(`./build-node-${nodeVersion}`)
}
},
{ LogOutput: process.stderr },
)
Replace the build.mjs
file from the previous step with the version below (highlighted lines indicate changes):
import { connect } from "@dagger.io/dagger"
// initialize Dagger client
connect(
async (client) => {
// Set Node versions against which to test and build
const nodeVersions = ["12", "14", "16"]
// get reference to the local project
const source = client.host().directory(".", { exclude: ["node_modules/"] })
// for each Node version
for (const nodeVersion of nodeVersions) {
// get Node image
const node = client.container().from(`node:${nodeVersion}`)
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application using specified Node version
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export(`./build-node-${nodeVersion}`)
}
},
{ LogOutput: process.stderr },
)
Replace the build.js
file from the previous step with the version below (highlighted lines indicate changes):
;(async function () {
// initialize Dagger client
let connect = (await import("@dagger.io/dagger")).connect
connect(
async (client) => {
// Set Node versions against which to test and build
const nodeVersions = ["12", "14", "16"]
// get reference to the local project
const source = client
.host()
.directory(".", { exclude: ["node_modules/"] })
// for each Node version
for (const nodeVersion of nodeVersions) {
// get Node image
const node = client.container().from(`node:${nodeVersion}`)
// mount cloned repository into Node image
const runner = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["npm", "install"])
// run tests
await runner.withExec(["npm", "test", "--", "--watchAll=false"]).sync()
// build application using specified Node version
// write the build output to the host
await runner
.withExec(["npm", "run", "build"])
.directory("build/")
.export(`./build-node-${nodeVersion}`)
}
},
{ LogOutput: process.stderr },
)
})()
This version of the CI tool has additional support for testing and building against multiple Node.js versions.
- It defines the test/build matrix, consisting of Node.js versions
12
,14
and16
. - It iterates over this matrix, downloading a Node.js container image for each specified version and testing and building the source application against that version.
- It creates an output directory on the host named for each Node.js version so that the build outputs can be differentiated.
Run the Node.js CI tool by executing the command below:
- TypeScript
- JavaScript (ESM)
- JavaScript (CommonJS)
dagger run node --loader ts-node/esm ./build.mts
dagger run node ./build.mjs
dagger run node ./build.js
The tool tests and builds the application against each version in sequence. At the end of the process, a built application is available for each Node.js version in a build-node-XX
folder in the project directory, as shown below:
tree -L 2 -d build-*
build-node-12
└── static
├── css
├── js
└── media
build-node-14
└── static
├── css
├── js
└── media
build-node-16
└── static
├── css
├── js
└── media
Conclusion
This tutorial introduced you to the Dagger Node.js SDK. It explained how to install the SDK and use it with a Node.js application. It also provided a working example of a Node.js CI tool powered by the SDK, demonstrating how to test an application against multiple Node.js versions in parallel.
Use the SDK Reference to learn more about the Dagger Node.js SDK.
Appendix A: Create a React application
Create a React application using the TypeScript template:
npx create-react-app my-app --template typescript
cd my-app