JuliusBrussee/caveman · error

caveman build: unresolved relative source import ${JSON.stri

Error message

caveman build: unresolved relative source import ${JSON.stringify(specifier)}

What it means

A relative import's candidate files (the specifier plus extension/index variants tried by the resolver) all returned ENOENT, so the source graph cannot find the imported module. The build requires a complete, finite file closure; an unresolved relative import makes the graph non-lockable and aborts with the failing specifier in the message.

Source

Thrown at packages/agent/src/source-graph.ts:84

      const candidates = extname(base)
        ? explicitImportCandidates(base, path)
        : [
          base,
          ...RESOLUTION_EXTENSIONS.map((extension) => base + extension),
          ...SOURCE_EXTENSIONS.map((extension) => resolve(base, `index${extension}`)),
        ];
      let found: string | undefined;
      for (const candidate of candidates) {
        try {
          await readFile(candidate);
          found = candidate;
          break;
        } catch (error) {
          if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
        }
      }
      if (!found) {
        throw new Error(`caveman build: unresolved relative source import ${JSON.stringify(specifier)}`);
      }
      if (bare) {
        await collectPackageClosure(packageRoot!, files, packageRoots);
        continue;
      }
      const lexicalFound = found;
      found = await realpath(found);
      const insideRoot = isPathWithin(canonicalRoot, found);
      if (!insideRoot && !bare && !external.has(path)) {
        const lexicalInsideRoot = isPathWithin(canonicalRoot, lexicalFound);
        if (lexicalInsideRoot) {
          throw new Error("caveman build: source graph symlink escapes project root");
        }
        throw new Error("caveman build: imported source escapes project root");
      }
      if (!insideRoot) external.add(found);
      if (!files.has(found)) {
        files.add(found);

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Check the specifier in the error message and create/rename the target file so one of the standard candidates exists (exact path, .ts/.js/.tsx variants, /index.*).
  2. Run the TypeScript compiler to surface the same broken import in your editor before building.
  3. On Linux, verify path casing matches the import exactly.

Example fix

// before: src/a.ts
import { x } from "./b/c"; // src/b/c.ts was moved to src/b/c/index.ts? or deleted

// after: restore the file at src/b/c.ts (or update the import to the real path)
Defensive patterns

Strategy: validation

Validate before calling

import { access } from "node:fs/promises";
async function relativeImportResolves(from: string, spec: string): Promise<boolean> {
  const base = resolve(dirname(from), spec);
  const candidates = extname(base)
    ? [base]
    : [base, `${base}.ts`, `${base}.tsx`, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.ts`, `${base}/index.js`];
  return access(candidates[0]!).then(() => true, () => false); // check each candidate similarly
}

Try / catch

try {
  await buildSourceGraph(root);
} catch (error) {
  if (error instanceof Error && error.message.startsWith("caveman build: unresolved relative source import")) {
    // the failing specifier is quoted in the message; create/rename the file or fix the path
  } else throw error;
}

Prevention

When it happens

Trigger: A source file imports "./utils/helper" when the file is helper.ts in a directory the candidate list does not cover, the file was renamed/deleted, or the specifier has a casing mismatch on case-sensitive filesystems.

Common situations: Renaming or moving a module without updating importers; case mismatches after cloning onto Linux from macOS/Windows; a missing file that TypeScript's own resolution tolerates via different fallback rules than this builder.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/4a48d96736824051. Report an issue: GitHub.