Skip to content
HomePagesHomePages template kit

no-node-builtins-in-contract

"No Node built-in imports in a section's contract files."

Venue: eslint — No Node built-in imports in a section’s contract files.

An import of a Node built-in module — anything node:-prefixed (node:fs, node:path, node:crypto, node:os, …) or a bare built-in name (fs, path, crypto, os, child_process, http, https, net, stream, util, url, worker_threads, …) — is banned in any of a section’s contract files: Renderer.tsx, section.ts and fixtures.ts (see Sections and their declarations).

The ban covers all three ways a specifier reaches a module:

  • a static import … from "node:fs",
  • a dynamic import("node:os"),
  • a require("node:path") call.

A subpath is still the same module: fs/promises is banned because its base, fs, is a built-in. Relative imports (./helpers.js) and npm dependencies (zod) are not built-ins and are not flagged by this rule.

A computation authored via compute() / derived is bundled into the section’s logic bundle, and that bundle is built with no external resolver — it resolves no external specifier at all. A Node built-in is exactly such an external specifier: it would fail to resolve when the bundle executes, and a computation that reached into fs or the crypto clock would not be pure, deterministic, or portable. The bundle must be pure and dependency-free, so the built-in import is banned at authoring time rather than discovered as a runtime failure.

Express the computation over the derive-time facts view f — every value it can legitimately need is already there, deterministically. If a value genuinely has to come from outside (a hash, a lookup), precompute it at fill time and pass it in as a slot value; the Renderer then just renders what it was given.

import { createHash } from "node:crypto";
import { compute } from "@homepages/template-kit";
export const slug = compute((f) => createHash("sha1").update(f.address).digest("hex"));
import { compute } from "@homepages/template-kit";
// Derived purely from the facts view — no external module, no runtime resolution.
export const slug = compute((f) =>
(f.property.address.display ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
);

If the value truly cannot be derived from f, compute it at fill time and declare a slot for it in the schema; the contract file then carries no built-in import at all.