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
- Rewrite computed dynamic imports to static string literals so they can be locked.
- For template literals with a static prefix, enumerate the possible modules in a literal map: const mods = { a: () => import("./a.js"), ... }.
- Use static import() entries per candidate instead of one computed path.
- 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
- Never interpolate variables or template expressions into import() paths.
- Use explicit loader maps enumerating statically importable modules.
- Lint with rules flagging dynamic imports without literal arguments.
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
- caveman build: aliased source loader is not lockable in
- caveman build: unresolved package dependency
- caveman build: unresolved relative source import
- caveman build: unresolved source dependency
- import export not found
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)