-
-
Notifications
You must be signed in to change notification settings - Fork 46
/
phx_live_storybook.ex
225 lines (185 loc) · 6.27 KB
/
phx_live_storybook.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
defmodule PhxLiveStorybook.BackendBehaviour do
@moduledoc """
Behaviour implemented by your backend module.
"""
alias PhxLiveStorybook.{FolderEntry, StoryEntry}
@doc """
Returns a configuration value from your config.exs storybook settings.
`key` is the config key
`default` is an optional default value if no value can be fetched.
"""
@callback config(key :: atom(), default :: any()) :: any()
@doc """
Returns a precompiled tree of your storybook stories.
"""
@callback content_tree() :: [%FolderEntry{} | %StoryEntry{}]
@doc """
Returns all the leaves (only stories) of the storybook content tree.
"""
@callback leaves() :: [%StoryEntry{}]
@doc """
Returns all the nodes (stoires & folders) of the storybook content tree as a flat list.
"""
@callback flat_list() :: [%FolderEntry{} | %StoryEntry{}]
@doc """
Returns an entry from its absolute storybook path (not filesystem).
"""
@callback find_entry_by_path(String.t()) :: %FolderEntry{} | %StoryEntry{}
@doc """
Retuns a storybook path from a story module.
"""
@callback storybook_path(atom()) :: String.t()
end
defmodule PhxLiveStorybook do
@external_resource "README.md"
@moduledoc @external_resource
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
alias PhxLiveStorybook.Entries
alias PhxLiveStorybook.ExsCompiler
alias PhxLiveStorybook.Stories.StoryValidator
require Logger
@doc false
defmacro __using__(opts) do
{opts, _} = Code.eval_quoted(opts, [], __CALLER__)
opts = merge_opts_and_config(opts, __CALLER__.module)
content_tree = content_tree(opts)
[
main_quote(opts),
recompilation_quotes(opts),
story_compilation_quotes(opts, content_tree),
config_quotes(opts),
stories_quotes(opts, content_tree)
]
end
defp merge_opts_and_config(opts, backend_module) do
config_opts = Application.get_env(opts[:otp_app], backend_module, [])
Keyword.merge(opts, config_opts)
end
defp main_quote(opts) do
quote do
@behaviour PhxLiveStorybook.BackendBehaviour
@impl PhxLiveStorybook.BackendBehaviour
def storybook_path(story_module) do
if Code.ensure_loaded?(story_module) do
content_path = Keyword.get(unquote(opts), :content_path)
file_path =
story_module.__file_path__()
|> String.replace_prefix(content_path, "")
|> String.replace_suffix(Entries.story_file_suffix(), "")
end
end
end
end
defp story_compilation_quotes(opts, content_tree) do
content_path = Keyword.get(opts, :content_path)
case compilation_mode(opts) do
:lazy ->
quote do
def load_story(story_path) do
story_path = String.replace_prefix(story_path, "/", "")
story_path = story_path <> Entries.story_file_suffix()
case ExsCompiler.compile_exs(story_path, unquote(content_path)) do
{:ok, story} -> StoryValidator.validate(story)
{:error, message, exception} -> {:error, message, exception}
end
end
end
:eager ->
quotes =
for story_entry <- Entries.leaves(content_tree) do
story_name = String.replace_prefix(story_entry.path, "/", "")
story_path = story_name <> Entries.story_file_suffix()
story =
story_path
|> ExsCompiler.compile_exs!(content_path)
|> StoryValidator.validate!()
quote do
@external_resource Path.join(unquote(content_path), unquote(story_path))
def load_story(unquote(story_name)) do
{:ok, unquote(story)}
end
end
end
quotes ++
[
quote do
def load_story(_), do: {:error, :not_found}
end
]
end
end
# This quote triggers recompilation for the module whenever a new file or any index file under
# content_path has been touched.
defp recompilation_quotes(opts) do
content_path =
Keyword.get_lazy(opts, :content_path, fn -> raise "content_path key must be set" end)
components_pattern = Path.join(content_path, "**/*")
index_pattern = Path.join(content_path, "**/*#{Entries.index_file_suffix()}")
quote do
@index_paths Path.wildcard(unquote(index_pattern))
@paths Path.wildcard(unquote(components_pattern))
@paths_hash :erlang.md5(@paths)
for index_path <- @index_paths do
@external_resource index_path
end
def __mix_recompile__? do
unquote(components_pattern) |> Path.wildcard() |> :erlang.md5() !=
@paths_hash
end
end
end
@doc false
defp stories_quotes(_opts, content_tree) do
entries = Entries.flat_list(content_tree)
leaves = Entries.leaves(content_tree)
find_entry_by_path_quotes =
for entry <- entries do
quote do
@impl PhxLiveStorybook.BackendBehaviour
def find_entry_by_path(unquote(entry.path)) do
unquote(Macro.escape(entry))
end
end
end
single_quote =
quote do
@impl PhxLiveStorybook.BackendBehaviour
def find_entry_by_path(_), do: nil
@impl PhxLiveStorybook.BackendBehaviour
def content_tree, do: unquote(Macro.escape(content_tree))
@impl PhxLiveStorybook.BackendBehaviour
def leaves, do: unquote(Macro.escape(Entries.leaves(leaves)))
@impl PhxLiveStorybook.BackendBehaviour
def flat_list, do: unquote(Macro.escape(entries))
end
find_entry_by_path_quotes ++ [single_quote]
end
defp content_tree(opts) do
content_path = Keyword.get(opts, :content_path)
folders_config = Keyword.get(opts, :folders, [])
Entries.content_tree(content_path, folders_config)
end
defp compilation_mode(opts) do
case Keyword.get(opts, :compilation_mode) do
mode when mode in [:lazy, :eager] ->
mode
_ ->
if Code.ensure_loaded?(Mix) and Mix.env() == :dev do
:lazy
else
:eager
end
end
end
@doc false
defp config_quotes(opts) do
quote do
@impl PhxLiveStorybook.BackendBehaviour
def config(key, default \\ nil) do
Keyword.get(unquote(opts), key, default)
end
end
end
end