Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Roblox internal Stories and Storybooks #29

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c2a3b04
Work from the day
vocksel Nov 6, 2024
7ec177d
Fix types
vocksel Nov 6, 2024
4dfd89e
Place raw errors on the next line
vocksel Nov 6, 2024
922c3c3
Support "group" field on storybooks
vocksel Nov 7, 2024
df83c1a
Mark roblox internal
vocksel Nov 7, 2024
2aeaf78
Existing renderers work with Roblox stories
vocksel Nov 7, 2024
404781a
Add mapStory support
vocksel Nov 7, 2024
c66fb44
Choose any random substory to render as the story
vocksel Nov 7, 2024
6c574ba
Use Prospector for immediate performance improvement
vocksel Nov 8, 2024
80069ce
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Nov 11, 2024
4d39e32
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Nov 21, 2024
a85500e
Add a todo
vocksel Nov 21, 2024
78af727
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Dec 5, 2024
0d179b0
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Dec 12, 2024
bac3658
Don't bother gating behind a flag
vocksel Dec 12, 2024
5963e56
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Dec 12, 2024
d075fc1
Handle more substory cases
vocksel Dec 16, 2024
d22446e
Adjust internal handling for React/ReactRoblox
vocksel Dec 16, 2024
d58594c
Fix mapStory for React storybooks
vocksel Dec 16, 2024
6be8b08
Fix build watching
vocksel Dec 16, 2024
e0b1716
Remove debug print
vocksel Dec 16, 2024
08a547d
Expose UnavailableStorybook type
vocksel Dec 16, 2024
a036d9a
Fix mapStory for Roact renderer
vocksel Dec 17, 2024
3611a65
Each Storybook has its own ModuleLoader for better sandboxing
vocksel Dec 18, 2024
23e599b
Storybooks keep track of their source module
vocksel Dec 18, 2024
a4afdd0
Fix unit tests and type errors
vocksel Dec 18, 2024
5026aea
Support `props` field on Stories
vocksel Dec 18, 2024
aab4408
Support mapDefinition
vocksel Dec 18, 2024
7de0209
Revise useStorybooks to better encapsulate loading
vocksel Dec 18, 2024
4a99804
Provide better interop with storyRoots
vocksel Dec 18, 2024
c054981
Each Storybook has its own loader
vocksel Dec 19, 2024
c7267cc
Typecasts
vocksel Dec 19, 2024
113dd99
Couple things to revert
vocksel Dec 19, 2024
1e42f86
Add some more tests
vocksel Dec 21, 2024
6ca12a8
Merge remote-tracking branch 'origin/roblox-internal' into roblox-int…
vocksel Dec 21, 2024
3e033c4
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Dec 24, 2024
78ac720
Merge remote-tracking branch 'origin/roblox-internal' into roblox-int…
vocksel Dec 24, 2024
9a00d17
Merge remote-tracking branch 'origin/main' into storybook-contains-th…
vocksel Dec 24, 2024
05cafb2
Unskip a test
vocksel Dec 24, 2024
ee649a4
Get useStorybooks tests passing
vocksel Dec 29, 2024
5c757f6
Get useStory tests passing
vocksel Dec 29, 2024
1157951
Merge branch 'storybook-contains-the-loader' into roblox-internal
vocksel Dec 29, 2024
46f1fd1
Merge remote-tracking branch 'origin/main' into roblox-internal
vocksel Dec 29, 2024
93e827f
Require useStorybooks inline
vocksel Dec 29, 2024
722387f
Remove implicit storyRoots
vocksel Dec 30, 2024
e85ae17
Mark Roblox internal behavior
vocksel Dec 30, 2024
c668640
Remove test for removed behavior
vocksel Dec 30, 2024
cef8dc1
Update story format docs
vocksel Dec 30, 2024
182fe0b
Mark another Roblox internal feature
vocksel Dec 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .lune/build.luau
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ if args.watch then
watch({
filePatterns = {
"src/.*%.luau",
"example/.*%.luau",
},
onChanged = build,
})
Expand Down
10 changes: 10 additions & 0 deletions foreman.toml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember to remove this before merging

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[tools]
rojo = { source = "rojo-rbx/rojo", version = "7.4.3" }
selene = { source = "Kampfkarren/selene", version = "0.27.1" }
StyLua = { source = "JohnnyMorganz/StyLua", version = "0.20.0" }
wally = { source = "UpliftGames/wally", version = "0.3.2" }
luau-lsp = { source = "JohnnyMorganz/luau-lsp", version = "1.34.0" }
wally-package-types = { source = "JohnnyMorganz/wally-package-types", version = "1.3.2" }
lune = { source = "lune-org/lune", version = "0.8.7" }
darklua = { source = "seaofvoices/darklua", version = "0.13.1" }
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
8 changes: 4 additions & 4 deletions src/e2e/e2e.spec.luau
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describeEach(storybookModules)("e2e %s", function(storybookModule)
} :: any
) :: ModuleLoader.ModuleLoader

local storybook = loadStorybookModule(mockModuleLoader, storybookModule)
local storybook = loadStorybookModule(storybookModule, mockModuleLoader)
local storyModules = findStoryModulesForStorybook(storybook)
local container

Expand Down Expand Up @@ -65,7 +65,7 @@ describeEach(storybookModules)("e2e %s", function(storybookModule)
assert(#storyModules >= 2, "storybook must have at least 2 stories")

for _, storyModule in storyModules do
local story = loadStoryModule(mockModuleLoader, storyModule, storybook)
local story = loadStoryModule(storyModule, storybook)

if story.controls then
numStoriesWithControls += 1
Expand All @@ -78,7 +78,7 @@ describeEach(storybookModules)("e2e %s", function(storybookModule)

describeEach(storyModules)("%s", function(storyModule)
test("basic mount/unmount lifecycle", function()
local story = loadStoryModule(mockModuleLoader, storyModule, storybook)
local story = loadStoryModule(storyModule, storybook)
local lifecycle = render(container, story)

afterRender()
Expand All @@ -91,7 +91,7 @@ describeEach(storybookModules)("e2e %s", function(storybookModule)
end)

test("rerenders on control changes", function()
local story = loadStoryModule(mockModuleLoader, storyModule, storybook)
local story = loadStoryModule(storyModule, storybook)

if story.controls then
local lifecycle = render(container, story)
Expand Down
3 changes: 3 additions & 0 deletions src/findStoryModulesForStorybook.spec.luau
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local JestGlobals = require("@pkg/JestGlobals")
local ModuleLoader = require("@pkg/ModuleLoader")

local findStoryModulesForStorybook = require("./findStoryModulesForStorybook")
local types = require("@root/types")
Expand Down Expand Up @@ -27,6 +28,8 @@ test("find all story modules for a storybook", function()

local storybook: types.LoadedStorybook = {
name = "Storybook",
loader = ModuleLoader.new(),
source = Instance.new("ModuleScript"),
storyRoots = {
container,
},
Expand Down
39 changes: 16 additions & 23 deletions src/hooks/useStory.luau
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
local ModuleLoader = require("@pkg/ModuleLoader")
local React = require("@pkg/React")

local loadStoryModule = require("@root/loadStoryModule")
Expand Down Expand Up @@ -28,11 +27,10 @@ local types = require("@root/types")
parent: Instance,
storyModule: ModuleScript,
storybook: Storybook,
loader: ModuleLoader,
})
local ref = useRef(nil :: Frame?)

local story = Storyteller.useStory(props.storyModule, props.storybook, props.loader)
local story = Storyteller.useStory(props.storyModule, props.storybook)

useEffect(function()
if ref.current then
Expand All @@ -54,32 +52,27 @@ local types = require("@root/types")
@tag React
@tag Story
@within Storyteller

]=]
local function useStory(
module: ModuleScript,
storybook: types.LoadedStorybook,
loader: ModuleLoader.ModuleLoader
): (types.LoadedStory<unknown>?, string?)
local function useStory(module: ModuleScript, storybook: types.LoadedStorybook): (types.LoadedStory<unknown>?, string?)
local state, setState = React.useState({} :: {
story: types.LoadedStory<unknown>?,
err: string?,
})

local loadStory = React.useCallback(function()
local story
local success, err = pcall(function()
story = loadStoryModule(loader, module, storybook)
end)

setState({
story = if success then story else nil,
err = if not success then err else nil,
})
end, { loader, module, storybook } :: { unknown })

React.useEffect(function()
local conn = loader.loadedModuleChanged:Connect(function(other)
local function loadStory()
local story
local success, err = pcall(function()
story = loadStoryModule(module, storybook)
end)

setState({
story = if success then story else nil,
err = if not success then err else nil,
})
end

local conn = storybook.loader.loadedModuleChanged:Connect(function(other)
if other == module then
loadStory()
end
Expand All @@ -90,7 +83,7 @@ local function useStory(
return function()
conn:Disconnect()
end
end, { module, loadStory, loader } :: { unknown })
end, { module, storybook } :: { unknown })

return state.story, state.err
end
Expand Down
15 changes: 7 additions & 8 deletions src/hooks/useStory.spec.luau
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ local act = ReactRoblox.act

type LoadedStorybook = types.LoadedStorybook

local loader: ModuleLoader.ModuleLoader
local container: Folder
local componentModule: ModuleScript
local storyModule: ModuleScript
local storybook: types.LoadedStorybook
local storybook: LoadedStorybook

beforeEach(function()
loader = ModuleLoader.new()

container = Instance.new("Folder")

componentModule = Instance.new("ModuleScript")
Expand Down Expand Up @@ -52,13 +49,15 @@ beforeEach(function()

storybook = {
name = "Storybook",
loader = ModuleLoader.new(),
source = Instance.new("ModuleScript"),
storyRoots = { container },
}
end)

test("loads a story module", function()
local get = renderHook(function()
return useStory(storyModule, storybook, loader)
return useStory(storyModule, storybook)
end)

local story, err = get()
Expand All @@ -77,7 +76,7 @@ test("handles story errors", function()
storyModule.Source = "syntax error" .. storyModule.Source

local get = renderHook(function()
return useStory(storyModule, storybook, loader)
return useStory(storyModule, storybook)
end)

local story, err = get()
Expand All @@ -88,7 +87,7 @@ end)

test("re-renders on module changes", function()
local get = renderHook(function()
return useStory(storyModule, storybook, loader)
return useStory(storyModule, storybook)
end)

local story, _err = get()
Expand Down Expand Up @@ -118,7 +117,7 @@ end)

test("reloads on dependency changes", function()
local get = renderHook(function()
return useStory(storyModule, storybook, loader)
return useStory(storyModule, storybook)
end)

local story = get()
Expand Down
138 changes: 100 additions & 38 deletions src/hooks/useStorybooks.luau
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
local ModuleLoader = require("@pkg/ModuleLoader")
local React = require("@pkg/React")
local Sift = require("@pkg/Sift")

local findStorybookModules = require("@root/findStorybookModules")
local isStorybookModule = require("@root/isStorybookModule")
local loadStorybookModule = require("@root/loadStorybookModule")
local types = require("@root/types")

type UnavailableStorybook = {
problem: string,
storybook: types.LoadedStorybook,
}
local useCallback = React.useCallback
local useEffect = React.useEffect
local useRef = React.useRef
local useState = React.useState

type ModuleLoader = ModuleLoader.ModuleLoader
type LoadedStorybook = types.LoadedStorybook
type UnavailableStorybook = types.UnavailableStorybook

--[=[
Performs all the discovery and loading of Storybook modules that would
Expand All @@ -32,9 +37,8 @@ type UnavailableStorybook = {

local function StorybookList(props: {
parent: Instance,
loader: ModuleLoader,
})
local storybooks = Storyteller.useStorybooks(props.parent, props.loader)
local storybooks = Storyteller.useStorybooks(props.parent)

local children = {}
for index, storybook in storybooks do
Expand Down Expand Up @@ -66,63 +70,121 @@ type UnavailableStorybook = {
@tag Storybook
@within Storyteller
]=]
local function useStorybooks(
parent: Instance,
loader: ModuleLoader.ModuleLoader
): {
available: { types.LoadedStorybook },
local function useStorybooks(parent: Instance): {
available: { LoadedStorybook },
unavailable: { UnavailableStorybook },
}
local storybooks, setStorybooks = React.useState({})
local unavailableStorybooks, setUnavailableStorybooks = React.useState({} :: { UnavailableStorybook })
local storybookConnections = useRef({} :: { [ModuleScript]: { RBXScriptConnection } })
local storybooks, setStorybooks = useState({} :: { LoadedStorybook })
local unavailableStorybooks, setUnavailableStorybooks = useState({} :: { UnavailableStorybook })

local getOrCreateConnectionObject = useCallback(function(storybookModule: ModuleScript): { RBXScriptConnection }
local existing = storybookConnections.current[storybookModule]
if existing then
return existing
else
local new = {}
storybookConnections.current[storybookModule] = new
return new
end
end, {})

local loadStorybooks = React.useCallback(function()
local newStorybooks = {}
local newUnavailableStorybooks: { UnavailableStorybook } = {}
local loadStorybook = useCallback(function(storybookModule: ModuleScript)
local connections = getOrCreateConnectionObject(storybookModule)
local loader = ModuleLoader.new()

for _, storybookModule in findStorybookModules(parent) do
local storybook: types.LoadedStorybook?
local reloadStorybook

local function load()
for _, connection in connections do
connection:Disconnect()
end

local storybook: LoadedStorybook?
local success, result = pcall(function()
storybook = loadStorybookModule(loader, storybookModule)
storybook = loadStorybookModule(storybookModule, loader)
end)

if success then
table.insert(newStorybooks, storybook)
table.insert(connections, loader.loadedModuleChanged:Connect(reloadStorybook))
table.insert(connections, storybookModule.AncestryChanged:Connect(reloadStorybook))

-- TODO: The logic to add/update storybooks at their index is nasty.
-- Revise this before merging
if success and storybook then
setStorybooks(function(prev)
local new = table.clone(prev)
local index = Sift.List.findWhere(new, function(other: LoadedStorybook)
return other.source == storybookModule
end)

if index then
new[index] = storybook
else
table.insert(new, storybook)
end

return new
end)
else
table.insert(newUnavailableStorybooks, {
local unavailableStorybook: UnavailableStorybook = {
problem = result,
storybook = {
name = storybookModule.Name,
loader = loader,
source = storybookModule,
storyRoots = {},
},
})
}

setUnavailableStorybooks(function(prev)
local new = table.clone(prev)
local index = Sift.List.findWhere(new, function(other: UnavailableStorybook)
return other.storybook.source == storybookModule
end)

if index then
new[index] = unavailableStorybook
else
table.insert(new, unavailableStorybook)
end

return new
end)
end
end

setStorybooks(newStorybooks)
setUnavailableStorybooks(newUnavailableStorybooks)
end, { parent, loader } :: { unknown })
function reloadStorybook(instance: Instance)
if instance == storybookModule and isStorybookModule(instance) then
for _, connection in connections do
connection:Disconnect()
end

React.useEffect(function()
local function reloadIfStorybook(instance: Instance)
if isStorybookModule(instance) then
loadStorybooks()
loader:clear()
load()
end
end

local connections = {
loader.loadedModuleChanged:Connect(reloadIfStorybook),
parent.DescendantAdded:Connect(reloadIfStorybook),
}
load()
end, {})

useEffect(function()
setStorybooks({})
setUnavailableStorybooks({})

loadStorybooks()
local storybookModules = findStorybookModules(parent)

for _, storybookModule in storybookModules do
task.spawn(loadStorybook, storybookModule)
end

return function()
for _, conn in connections do
conn:Disconnect()
for _, connections in storybookConnections.current do
for _, connection in connections do
connection:Disconnect()
end
end
end
end, { loadStorybooks, loader, parent } :: { unknown })
end, { parent } :: { unknown })

return {
available = storybooks,
Expand Down
Loading
Loading