[bldr]docs
Guides

Node projects

Dependency installs cached apart from builds, generated code layered in, and the result served.

nodeProject builds anything with a package.json. The design point worth understanding before anything else: installing dependencies and building are separate, separately cached runs.

import { nodeProject } from "bldr/node-tools";
import nodeImage from "node";

const dist = nodeProject({
    scope,
    image: nodeImage,
    source: member.rootDirectory,
}).build({ outDir: "dist" });

scope.addOutputDirectory("site", dist);

npm ci runs over the manifests alone, so its cache key is your lockfile. It survives every source edit. Editing a component re-runs the build and not the install, which is the difference between a fast loop and a slow one.

The install is also the step with internet access. Build, test and lint pods have none: they see node_modules as the install left it, and a script that downloads at build time (npx of a package that is not a dependency, a browser fetched on first run) fails there.

Run a different script

.build({ outDir: "build/client", script: "build:prod" })

script is the package.json script, outDir is what to keep.

Layer in generated code

Code you generate belongs in the tree at content level, not copied in by a command inside the container:

nodeProject({
    scope,
    image: nodeImage,
    source: member.rootDirectory,
    generated: [{ tree: apiClient(), dest: "src/gen" }],
});

The project sees src/gen as if it were checked in, and the generated tree is part of the cache key, so regenerating the client rebuilds what depends on it.

Depend on another member's package

import { pkg as designSystem } from "bldr/design-system";

nodeProject({
    scope,
    image: nodeImage,
    source: member.rootDirectory,
    deps: { "@bldr/design-system": designSystem() },
});

The dependency is vendored under .bldr-deps/ and your package.json refers to it as a file: dependency, so npm resolves it normally.

Serve it

import { staticSite } from "bldr/node-tools";
import nginxImage from "nginx";

scope.addRunnable("site", staticSite({
    image: nginxImage,
    site: dist,
    port: 8080,
    fallback: "/index.html",   // for a single-page app
}));

Tests as a target

import { fromJestJson } from "bldr";

const testOut = nodeProject({ scope, image: nodeImage, source: member.rootDirectory })
    .build({ outDir: "test-out", script: "test:json" });

scope.addTest("unit", fromJestJson(testOut, "**/*.json"));

Have the script write a JSON report and end with || true, so a failing case becomes data in the report rather than a failed pod. The test target then reports per-case results instead of just failing.

When the lockfile goes stale

Adding a dependency to package.json without updating the lockfile makes npm ci fail with Missing: … from lock file. Only npm can write that file, so refresh it with a build:

bldr build --export <member>/lockfile:members/<dir>

That runs npm install --package-lock-only and writes the refreshed lock back into your member.

On this page