Using PlutoLinks @ingredients with Julia Modules: Gotcha and Workaround

While setting up a Pluto notebook that depends on helper functions defined in an external script, I ran into a subtle behavior of PlutoLinks.@ingredients that’s worth documenting.

The goal

I wanted to keep reusable functions in an external script (scripts/MyModule.jl) and load them reactively into a Pluto notebook so that edits to the file automatically trigger re-evaluation of dependent cells, which is the main appeal of @ingredientsover a plain include.

Basic usage

using PlutoLinks: @ingredients
M = @ingredients "../scripts/MyModule.jl"

@ingredients watches the file for changes and re-runs dependent cells reactively. Great.

The gotcha: double-nesting when the script defines a module

If your script is a plain Julia file (no module wrapper), functions are accessible directly:

# scripts/MyModule.jl — no module wrapper
function my_func(x)
    x + 1
end

In Pluto (these statements should go in different chunks)

M = @ingredients "../scripts/MyModule.jl"
M.my_func(1)  # ✅ works

However, if your script wraps its content in a module, @ingredients wraps the entire script in its own namespace too, resulting in double-nesting:

# scripts/MyModule.jl — with module wrapper
module MyModule
export my_func
function my_func(x)
    x + 1
end
end #module

Then, in Pluto:

M = @ingredients "../scripts/MyModule.jl"
M.my_func(1)        # ❌ UndefVarError
M.MyModule.my_func(1)  # ✅ works, but awkward

Of course you can drop the module wrapper in the script and write plain Julia, but IMHO it’s better to keep the module (useful if you also include the file elsewhere or plan to turn it into a proper package), and alias it in the notebook:

M = @ingredients "../scripts/MyModule.jl"
const MyModule = M.MyModule
MyModule.my_func(1)  # ✅ clean

Hope this saves someone an hour of head-scratching!

GL