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.
Reason
Section titled “Reason”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.
Before
Section titled “Before”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.
See also
Section titled “See also”- Server vs client — how a file is scoped, and what an island may do.
- Rules index — the full rule corpus.