"Unsupported Path" warning when requiring a module referenced in a table as opposed to a local variable

Reproduction Steps

Repro Explanation

UnknownRequireSimpleRepro.rbxl (34.6 KB)

In this simplest repro place, I have a script in ReplicatedStorage (called “ModuleA”), which requires another module (“ModuleB”) in five different ways.

The first two are the simplest—directly requiring ModuleB from a relative path and then an absolute path:

--!strict

local ModuleB_fromScript = require(script.Parent.ModuleB)
local ModuleB_fromGame = require(game.ReplicatedStorage.ModuleB)

The third method is also simple, this time localizing ReplicatedStorage instead of referencing it from game or script:

local ReplicatedStorage = game.ReplicatedStorage
local ModuleB_fromLocalizedContainer = require(ReplicatedStorage.ModuleB)

The fourth example, however, simply stores ModuleB in a table rather than in a local variable. This emits an “unsupported path” warning from luau:

local Modules = {
	ModuleB = game.ReplicatedStorage.ModuleB
}

local ModuleB_fromTableOfModules = require(Modules.ModuleB)

The final example stores ReplicatedStorage in a table with the same issue:

local Containers = {
	ReplicatedStorage = game.ReplicatedStorage
}

local ModuleB_fromTableOfModules = require(Containers.ReplicatedStorage.ModuleB)


My use case for this

Having a large-scale modular codebase in Roblox runs into some issues with boilerplate, slowing down the process of writing new code.
For one thing, you must always split server code and client code into entirely different folders if you want to secure server-side ModuleScripts. Secondly, you also have to split experience-wide code and place-specific code into entirely different folder trees if you want to be able to manage shared code across a multi-place experience.

This lends itself to a lot of boilerplate to prevent require statements that exceed 100 lines; my modules typically have a 3-5 line boilerplate block before any require statements:

This follows Roblox’s official (though antiquated) convention of module organization that states you should put service require statements (and localizing instances in a variable) before require statements in code.

It would be much simpler if I could instead store these paths in a module called “src” in ReplicatedStorage! This way, it would be much easier to organize require paths and quickly write new code in my framework with absolutely no boilerplate other than requiring the src module in a single <80 character line.

The “src” module in my codebase would likely look something like the following:

--!strict
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local src = {}

if RunService:IsServer() then
    local ServerScriptService = game:GetService("ServerScriptService")
    src.Server = {
        Game = ServerScriptService.ServerFramework, -- This holds experience-wide server code
        Place = ServerScriptService.ServerPlace, -- This holds place-specific server code
    }
end

src.Client = {
    Game = ReplicatedStorage.ClientFramework, -- This holds experience-wide client code
    Place = ReplicatedStorage.ClientPlace, -- This holds place-specific client code
}

src.Shared = {
    Game = ReplicatedStorage.SharedFramework, -- This holds experience-wide code that both the server and client should use
    Place = ReplicatedStorage.SharedPlace, -- This holds place-specific code that both the server and client should use
}

return src

Then, I could use another module to reference a path to my module:

--!strict

-- This is the only boilerplate I need to interface with my codebase now:
local src = require(game.ReplicatedStorage.src)

-- Unfortunately, these show an "unsupported path" warning right now even though
-- the auto-complete widget shows them as valid paths:
local OutfitEditorConstants = require(src.Shared.Game.Constants.OutfitEditorConstants)
local AvatarContentConstants = require(src.Shared.Game.Constants.AvatarContentConstants)
local TeleportUtil = require(src.Server.Game.Util.TeleportUtil)

Expected Behavior
Requiring paths that start from/are just instances referenced in a table should have the same effect as requiring paths referencing instances in a local variable, and this should work across modules too.

Actual Behavior
An “unsupported path” warning is emitted; whether this is the intentional behavior or not now, it should definitely be supported in the long term. After all, the autocompletion widget even seems to detect the children of the instances stored in another module’s exported table.

Workaround
Having a 3-5 line boilerplate on every script/module is my workaround.

Issue Area: Studio
Issue Type: Other
Impact: Moderate
Frequency: Constantly
Date First Experienced: 2021-08-01 00:08:00 (-06:00)
Date Last Experienced: 2022-02-13 00:02:00 (-07:00)

3 Likes

Please don’t respond to bug reports if you have nothing meaningful to add. A workaround is already mentioned in the post.

This is more of a feature request and less of a bug report. I doubt we will move in this direction though - if anything we’re looking into the opposite directions (by looking into ways to make require less dynamic). In particular, tracking module references across modules results in odd dependencies in the graph discovery itself - now to resolve references inside a given module, we would need to completely resolve references in dependent modules! That creates more opportunity for difficult-to-break cycles and makes the discovery process more fragile / less performant.

FWIW we know that the need for putting sever-side ModuleScripts into separate services is a problem. It’s on our radar to solve by being able to somehow designate module scripts as server-only so that you can put all your scripts in a single container.

8 Likes