Skip to content

Add custom module resolvers for import() #7566

@iarebatman

Description

@iarebatman

Is your feature request related to a problem? Please describe.

What problem does this solve?

I'd like to be able to provide Xmake modules from sources other than normal .lua files in module directories, while keeping the caller experience the same:

import("tools.foo")

Some examples:

  • generated Lua modules
  • virtual/test modules
  • transpiled modules from another Lua-targeting language
  • project-local module providers that do not map cleanly to static files

Right now, import(...) is effectively bound to module files and module directories. That works well for normal cases, but it makes generated or dynamic modules awkward because they need to exist as physical files before project/script code can import them.

Why this seems useful

This keeps import(...) as the single user-facing module API while allowing advanced projects to provide generated or virtual modules cleanly.

The feature is intentionally generic and is not tied to any specific language or generator.

Describe the solution you'd like

Proposed API

Add a project API:

add_moduleresolver(function (name, ctx)
    if name == "virtual.hello" then
        return ctx.module({
            hello = function ()
                return "hello"
            end
        })
    end
end)

Then normal imports continue to work:

local hello = import("virtual.hello", {anonymous = true})
print(hello.hello())

For generated file-backed modules:

add_moduleresolver(function (name, ctx)
    local generated = try_generate_module(name)
    if generated then
        return ctx.file(generated)
    end
    return ctx.miss()
end)

Resolution order

Resolvers should be fallback providers:

  1. normal module lookup
  2. registered module resolvers, in registration order
  3. normal import failure

This preserves existing behavior and avoids accidentally shadowing built-in or project-local modules.

Expected behavior

  • resolvers are only called after normal module lookup fails
  • resolver results are cached by logical module name
  • nocache bypasses resolver result caching
  • ctx.file(path) loads through the existing module loader
  • ctx.module(table) provides an in-memory module export table
  • ctx.miss() explicitly skips the resolver

Describe alternatives you've considered

Alternatives considered

I looked at a few existing approaches before implementing this:

1. Pre-generating modules before running Xmake

This works for some cases, but it pushes the responsibility outside of Xmake itself. The generated modules must already exist before any project logic can import them, which makes workflows involving generated or dynamic modules awkward.

It also complicates caching/invalidation because generation becomes disconnected from the import system.

2. Generating modules through custom targets/hooks

I considered using existing hooks such as before_build, before_load, etc. to generate Lua modules into module directories.

The main issue is lifecycle timing. import(...) is used very early during project/script evaluation, before most build hooks run. This means generated modules still need to exist ahead of time or require awkward bootstrap logic.

This also tends to pollute projects with helper targets/scripts whose only purpose is preparing importable modules.

3. Extending add_moduledirs()

I considered whether this could be modeled entirely as additional module search directories.

That works for static/generated files, but it still assumes modules exist as filesystem-backed Lua files and does not support:

  • virtual modules
  • dynamically synthesized modules
  • generated modules that do not naturally map to static files
  • alternate module providers

Why a resolver API seemed cleaner

A resolver API keeps all of this behind the existing import(...) interface while preserving normal module semantics and precedence.

The caller still just writes:

import("tools.foo")

and the project can decide how that module is provided.

The implementation is intentionally fallback-only so existing behavior remains unchanged unless a module cannot already be resolved normally.

Additional context

Implementation PR with Tests: #7569
Documentation PR: xmake-io/xmake-docs#279

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions