JuliusBrussee/caveman · error

caveman build: computed source dependency is not lockable in

Error message

caveman build: computed source dependency is not lockable in ${JSON.stringify(path)}

What it means

esmSourceSpecifiers() throws when an import/export declaration has no statically known specifier string (es-module-lexer reports n === undefined for it). The build lock must enumerate every dependency edge as a literal; a computed dependency (e.g. import(someVariable) or export * from template) cannot be locked, so the file path is reported and the build aborts.

Source

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

  return child === "" || (!isAbsolute(child) && child !== ".." &&
    !child.startsWith("../") && !child.startsWith("..\\"));
}

function esmSourceSpecifiers(source: string, path: string): string[] {
  let imports: ReturnType<typeof parse>[0];
  try {
    [imports] = parse(source, path);
  } catch (error) {
    throw new Error(
      `caveman build: source module syntax is not lexable in ${JSON.stringify(path)}`,
      { cause: error },
    );
  }
  const specifiers: string[] = [];
  for (const item of imports) {
    if (item.d === -2) continue; // import.meta is metadata, not a dependency.
    if (item.n === undefined) {
      throw new Error(`caveman build: computed source dependency is not lockable in ${JSON.stringify(path)}`);
    }
    specifiers.push(item.n);
  }
  return specifiers;
}

function typescriptSourceSyntax(
  source: string,
  path: string,
  code: Uint8Array,
): { lexableSource: string; specifiers: string[] } {
  if (!TYPESCRIPT_SOURCE_EXTENSIONS.has(extname(path))) {
    return { lexableSource: source, specifiers: [] };
  }
  const masks: Array<{ start: number; end: number }> = [];
  const specifiers: string[] = [];
  let precedingExportEnd: number | undefined;
  for (const match of source.matchAll(TYPESCRIPT_IMPORT_EXPORT_PATTERN)) {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Replace computed imports with a static map of literal specifiers and select from it at runtime.
  2. If only some paths are possible, enumerate them all as literal imports (optionally via a generated file).
  3. Move truly dynamic loading behind an API boundary that stays outside the locked source graph.

Example fix

// before
const mod = await import(`./locales/${locale}.ts`);

// after
const locales = { en: () => import("./locales/en.ts"), de: () => import("./locales/de.ts") };
const mod = await locales[locale]();
Defensive patterns

Strategy: validation

Validate before calling

import { parse } from "es-module-lexer";
function hasComputedImports(source: string, path: string): boolean {
  const [imports] = parse(source, path);
  return imports.some((item) => item.d !== -2 && item.n === undefined);
}
if (hasComputedImports(src, file)) throw new Error(`computed import in ${file}`);

Type guard

const isStaticSpecifier = (item: { d: number; n: string | undefined }): boolean =>
  item.d === -2 || item.n !== undefined;

Try / catch

try {
  await buildSourceGraph(root);
} catch (error) {
  if (error instanceof Error && error.message.includes("computed source dependency is not lockable")) {
    // replace import(variable) with a literal specifier map
  } else throw error;
}

Prevention

When it happens

Trigger: Dynamic import with a non-literal argument: const mod = await import(locale); re-export from a computed string; import.meta.dynamic-ish patterns that defeat static analysis.

Common situations: Plugin/lazy-loading systems that build module names at runtime; i18n loaders importing locale files by variable; refactors that replace static imports with data-driven ones.

Related errors


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