denoland/deno · error
workspace root is not a local directory
Error message
workspace root is not a local directory
What it means
The native type checker derives a project root path from the workspace's root_dir_url via Url::to_file_path (cli/tools/check.rs:68-74); the conversion fails when the root is not a file:// URL. Native check materializes tsconfigs and syncs types under that directory, so a non-local workspace root cannot be checked.
Source
Thrown at cli/tools/check.rs:73
check_flags: CheckFlags,
) -> Result<(), AnyError> {
if check_flags.doc || check_flags.doc_only {
// Doc snippet extraction was handled by Deno 2.x's forked tsc; the native
// compiler does not type-check markdown/JSDoc snippets yet.
log::warn!(
"{} --doc/--doc-only is not yet supported by the native type checker and will be ignored",
colors::yellow("Warning")
);
}
let factory = CliFactory::from_flags(flags.clone());
let cli_options = factory.cli_options()?;
let project_root = cli_options
.workspace()
.root_dir_url()
.to_file_path()
.map_err(|_| {
deno_core::anyhow::anyhow!("workspace root is not a local directory")
})?;
// Build the module graph over the requested roots (or the whole project).
// Deno owns resolution: this drives deno's own graph diagnostics (missing
// modules + hints) and the incremental type-check cache, so we can skip the
// external compiler entirely when nothing it sees has changed.
// Resolve the requested roots the same way `deno check` always has:
// globs are expanded, workspace `exclude` is applied, and
// `include_ignored_specified: false` means an explicitly-passed excluded file
// is skipped rather than force-checked. An empty result is not an error - it
// just means there's nothing to check (e.g. every match was excluded), which
// deno reports as a warning and a clean exit.
let graph_container = factory.main_module_graph_container().await?;
let mut roots = graph_container.collect_specifiers(
&check_flags.files,
crate::graph_container::CollectSpecifiersOptions {
include_ignored_specified: false,
},View on GitHub (pinned to 89f33cbef2)
Solutions
- Run deno check from a local directory with a local deno.json/workspace (the root must map to a real directory)
- Clone/download the remote-configured project locally and check it there
- If you believe the root is local yet this fires, check for cwd weirdness (deleted directory, exotic mount) and re-run from a stable path
Example fix
# before: workspace rooted at a remote config
deno check --config https://example.com/deno.json src/
# after: local config
Deno.writeTextFileSync('deno.json', await (await fetch('https://example.com/deno.json')).text());
deno check --config deno.json src/ Defensive patterns
Strategy: type-guard
Validate before calling
// Verify the workspace root is a local directory before native check
function assertLocalWorkspaceRoot(rootUrl: string) {
if (new URL(rootUrl).protocol !== 'file:') {
throw new Error(`native check requires a local workspace root: ${rootUrl}`);
}
} Type guard
function isLocalFileUrl(url: string): url is `file://${string}` {
try { return new URL(url).protocol === 'file:'; } catch { return false; }
} Prevention
- Run deno check with a local deno.json in a local directory
- Vendor remote configs into the repo before type-checking
When it happens
Trigger: A workspace whose root resolves to a non-file scheme - e.g. a config/workspace rooted at a remote URL rather than a local directory; unusual embedded/virtual filesystem environments where the root URL lacks a local path.
Common situations: Experiments driving deno check against remote-configured workspaces; harnesses running Deno in sandboxes whose cwd/root is not representable as a local path.
Related errors
- Unexpected third argument to Deno.bench()
- BenchContext::end() has already been invoked
- ${prefix}Linter plugin must be an object
- ERR_FS_CP_UNKNOWN
- ERR_INVALID_ARG_TYPE
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/753b5fba7be78eb0.
Report an issue: GitHub.