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

Thrown by esmSourceSpecifiers when the lexer reports an import whose specifier string could not be statically determined (item.n === undefined, e.g. dynamic import(importExpr) or import(variable)). The build requires a concrete, lockable specifier for every dependency; computed ones are rejected rather than guessed. import.meta (d === -2) is explicitly exempted as metadata.

Solutions

  1. Rewrite computed dynamic imports to static string literals so they can be locked.
  2. For template literals with a static prefix, enumerate the possible modules in a literal map: const mods = { a: () => import("./a.js"), ... }.
  3. Use static import() entries per candidate instead of one computed path.
  4. If the module is truly runtime-only, load it outside the locked graph (e.g. via a runtime loader) rather than a source dependency.

Example fix

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

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

Strategy: validation

Validate before calling

// Ensure every dynamic import in source uses a string literal
function hasOnlyLiteralDynamicImports(source) {
  return !/import\s*\(\s*[^"']/.test(source);
}

Type guard

null

Try / catch

try {
  await graph.expand();
} catch (e) {
  if (e instanceof Error && e.message.includes("not lockable")) {
    console.error("Rewrite computed dynamic imports to static string literals or an explicit module map.");
  }
  throw e;
}

Prevention

When it happens

Trigger: specifiers -> esmSourceSpecifiers iterates lexer results and finds a dynamic import whose `n` (specifier) is undefined because the argument is an expression, not a string literal: await import(bases[name]), import(`./x/${id}`), etc.

Common situations: Dynamic imports with computed paths (plugin loaders, i18n locale loading, route code-splitting); template-literal import paths; imports from a config map.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/ed7a3d1345d9ab59. Report an issue: GitHub.

Appendix: 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 3ee70a1026)