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)