How Cordis Resolves Context Base URLs and Handles Path Resolution

Cordis resolves context base URLs through a mutable baseUrl property on the Context object, which serves as the reference point for all relative module imports and file-system paths throughout the framework.

The Cordis framework implements a flexible, runtime-adjustable mechanism for path resolution. Every Context instance carries a baseUrl that plugins can read and update, ensuring that relative imports always resolve correctly regardless of where configuration files or entry points are located.

The Context Base URL Mechanism

At the heart of Cordis path resolution lies the Context class defined in packages/core/src/context.ts. The baseUrl field is declared in the Context interface and initialized to undefined in the constructor.

This mutable property allows the framework to establish a dynamic resolution context. Rather than relying on static configuration or environment variables, Cordis tracks resolution state directly on the context object that flows through every plugin.

Setting the Initial Base URL

When the loader plugin instantiates, it accepts an optional baseUrl configuration option. If provided, the loader assigns it directly to the current context:

if (config.baseUrl) {
  this.ctx.baseUrl = config.baseUrl
}

Source: packages/loader/src/index.ts

This assignment typically occurs during application bootstrap. Users commonly set baseUrl to the directory of the main entry script:

import { Context } from 'cordis'
import { Loader } from '@cordisjs/plugin-loader'

const ctx = new Context()
new Loader(ctx, { baseUrl: import.meta.url })

Dynamic Base URL Updates via the Include Plugin

The include plugin demonstrates how Cordis handles nested configuration files. When loading a configuration file, the include plugin:

  1. Resolves the config file path relative to the current ctx.baseUrl
  2. Rewrites ctx.baseUrl to the directory containing that file
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl))
this.ctx.baseUrl = new URL('.', pathToFileURL(this.filename)).href

Source: packages/include/src/index.ts

This ensures that subsequent relative imports inside the configuration file resolve against the config file's directory rather than the original base URL. The context's resolution point shifts dynamically as the application structure is discovered.

Module Import Resolution in the Loader Tree

When the loader imports a module by name, it constructs a URL object using the current ctx.baseUrl:

return await import(/* @vite-ignore */new URL(name, this.ctx.baseUrl).href)

Source: packages/loader/src/config/tree.ts

If the module name is already an absolute URL, the base URL is ignored per standard URL constructor behavior. Otherwise, the relative name resolves against ctx.baseUrl to produce an absolute import path.

Hot Module Replacement Path Handling

The HMR subsystem also depends on ctx.baseUrl for filesystem operations. It computes a module's directory (baseDir) using:

fileURLToPath(new URL(config.base || '.', ctx.baseUrl))

Source: packages/hmr/src/index.ts

This conversion from URL to filesystem path enables file-watching and recompilation to function correctly regardless of where the application was started.

Complete Resolution Flow

Understanding how Cordis handles base URL resolution requires tracing the complete lifecycle:

  • Initialization: Application starts with ctx.baseUrl as undefined
  • Loader setup: If configured, Loader assigns the initial baseUrl
  • Configuration loading: Include plugin may replace baseUrl with a config file's directory
  • Runtime imports: All relative imports resolve against the current ctx.baseUrl
  • Development tools: HMR and utilities read ctx.baseUrl for filesystem paths

Practical Implementation Example

import { Context } from 'cordis'
import { Loader } from '@cordisjs/plugin-loader'
import { Include } from '@cordisjs/plugin-include'

const ctx = new Context()

// 1. Establish initial base URL from entry script
new Loader(ctx, { baseUrl: import.meta.url })

// 2. Load config file—the include plugin adjusts baseUrl automatically
new Include(ctx, {
  path: './config.yml',  // resolved relative to initial baseUrl
})

// 3. After Include loads, ctx.baseUrl points to config.yml's directory

// 4. Import modules relative to the current baseUrl
await import(/* @vite-ignore */new URL('my-plugin.js', ctx.baseUrl).href)

Summary

Frequently Asked Questions

What happens if no baseUrl is configured?

The baseUrl property remains undefined and relative URL resolution falls back to the current working directory or fails explicitly depending on the operation. The include plugin or explicit loader configuration typically provides the initial value before relative imports occur.

Can plugins override baseUrl after initialization?

Yes. Any plugin with access to the context can read and write ctx.baseUrl. The include plugin demonstrates this pattern by updating the base URL when loading nested configuration files, ensuring subsequent imports resolve correctly.

How does Cordis handle absolute versus relative paths?

Absolute URLs (those with protocols like file:// or https://) pass through unchanged. Relative paths always resolve against ctx.baseUrl using the standard URL constructor, which ignores the base when the input is already absolute.

Is baseUrl used for both ESM imports and filesystem operations?

Correct. ESM dynamic imports use new URL(name, ctx.baseUrl).href directly. Filesystem operations in HMR and other utilities convert the URL to a path using fileURLToPath() after resolution against ctx.baseUrl.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →