lookbook 0.7.0 → 0.7.2.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40ec6640c4c188089ff874cb11fb129ddc6c541d078681b1b3dea133f3afaedf
4
- data.tar.gz: 331f4693438b931929b40d58c4b7c0d0cf0ece9089891d8e5863c8971f5e4138
3
+ metadata.gz: 7c26bf5a4901bcb0fd09b8742ae3e9c2df8f8c337c3f312152cb31dc6a74aa23
4
+ data.tar.gz: e1e8a016b29cf87357c3b291c99eaaf8d1e21d1a4acb552bab7777699579f265
5
5
  SHA512:
6
- metadata.gz: a538f52d85d4d5e833c2549442b001e44e749b42e87a5bbe4676e718300405b7b9b32d6efd18b1ce23dedcfefce8c82a4f4128236fa83dcff570b7f609b15004
7
- data.tar.gz: a5a6fe4dbba3e951441b5c4fb64b6a268de687c99bb7779892d5e819eeae19fbcee8d740c749808671bf65202fc6af581a958869f3fb67c6f9aac5c5af364007
6
+ metadata.gz: dd771d407e79c1958ad0c4873fa242d7c3ad345091291275c5efd261be1d4617d8e60a6a0e82da97f2e01da8aa3a4781f169c13b1559dd481a1939598585b8d1
7
+ data.tar.gz: d4a40a44f2864c7c9b7bdb3b849092973619044fae370e056dda42b7382bc324fcc6932b26b4a15962d283fa8030782bd3a37e0f1e33c50f4aaf5046ba5c0ccd
@@ -93,7 +93,7 @@
93
93
  .code.numbered:before {
94
94
  content: "";
95
95
  left: 2.7em;
96
- @apply absolute top-0 bottom-0 border-r border-gray-200;
96
+ @apply absolute top-0 bottom-0 border-r border-gray-200 z-10;
97
97
  }
98
98
 
99
99
  .code.numbered .line {
@@ -115,8 +115,25 @@
115
115
  @apply flex-none pr-4;
116
116
  }
117
117
 
118
+ .code.focussed .line:not(.highlighted-line) *,
119
+ .code.focussed .line:not(.highlighted-line) .line-content * {
120
+ @apply !text-gray-700 !text-opacity-40;
121
+ }
122
+
123
+ .code.focussed .line:not(.highlighted-line) .line-number {
124
+ @apply !opacity-70;
125
+ }
126
+
127
+ .code.focussed .line.highlighted-line {
128
+ @apply bg-yellow-50;
129
+ }
130
+
131
+ .code.focussed .line.highlighted-line .line-number {
132
+ @apply text-gray-900;
133
+ }
134
+
118
135
  .prose .code {
119
- @apply !bg-white border border-gray-300 text-gray-600 my-8 text-sm rounded-md py-4 overflow-auto max-w-3xl w-full mx-auto;
136
+ @apply !bg-white border border-gray-300 text-gray-600 my-8 rounded-md py-4 text-sm overflow-auto max-w-3xl w-full mx-auto;
120
137
  }
121
138
 
122
139
  .prose .code:not(.numbered) .line {
@@ -18,12 +18,24 @@ module Lookbook
18
18
  @pages = Lookbook.pages
19
19
  @page = @pages.find_by_path(params[:path])
20
20
  if @page
21
- @page_content = page_controller.render_page(@page)
22
- @next_page = @pages.find_next(@page)
23
- @previous_page = @pages.find_previous(@page)
24
- @title = @page.title
21
+ if @page.errors.any?
22
+ render "lookbook/error", locals: {error: @page.errors.first}
23
+ else
24
+ begin
25
+ @page_content = page_controller.render_page(@page)
26
+ @next_page = @pages.find_next(@page)
27
+ @previous_page = @pages.find_previous(@page)
28
+ @title = @page.title
29
+ rescue => exception
30
+ render "lookbook/error", locals: {
31
+ error: Lookbook::Error.new(exception, {
32
+ file_path: @page.full_path,
33
+ source_code: @page.content
34
+ })
35
+ }
36
+ end
37
+ end
25
38
  else
26
- @title = "Not found"
27
39
  render "not_found"
28
40
  end
29
41
  end
@@ -1,12 +1,5 @@
1
1
  module Lookbook
2
2
  class PreviewsController < ApplicationController
3
- EXCEPTIONS = [
4
- ViewComponent::PreviewTemplateError,
5
- ViewComponent::ComponentError,
6
- ViewComponent::TemplateError,
7
- ActionView::Template::Error
8
- ]
9
-
10
3
  def self.controller_path
11
4
  "lookbook/previews"
12
5
  end
@@ -17,7 +10,14 @@ module Lookbook
17
10
  def preview
18
11
  if @example
19
12
  set_params
20
- render html: render_examples(examples_data)
13
+ begin
14
+ render html: render_examples(examples_data)
15
+ rescue => exception
16
+ render_in_layout "lookbook/error",
17
+ layout: "lookbook/basic",
18
+ error: prettify_error(exception),
19
+ disable_header: true
20
+ end
21
21
  else
22
22
  render_in_layout "not_found"
23
23
  end
@@ -30,8 +30,8 @@ module Lookbook
30
30
  @examples = examples_data
31
31
  @drawer_panels = drawer_panels.filter { |name, panel| panel[:show] }
32
32
  @preview_panels = preview_panels.filter { |name, panel| panel[:show] }
33
- rescue *EXCEPTIONS
34
- render_in_layout "error"
33
+ rescue => exception
34
+ render_in_layout "lookbook/error", error: prettify_error(exception)
35
35
  end
36
36
  else
37
37
  render_in_layout "not_found"
@@ -152,8 +152,31 @@ module Lookbook
152
152
  @preview_controller ||= controller
153
153
  end
154
154
 
155
- def render_in_layout(path)
156
- render "not_found", layout: params[:lookbook_embed] ? "lookbook/basic" : "lookbook/application"
155
+ def render_in_layout(path, layout: nil, **locals)
156
+ render path, layout: layout.presence || (params[:lookbook_embed] ? "lookbook/basic" : "lookbook/application"), locals: locals
157
+ end
158
+
159
+ def prettify_error(exception)
160
+ error_params = if exception.is_a?(ViewComponent::PreviewTemplateError)
161
+ {
162
+ file_path: @preview&.full_path,
163
+ line_number: 0,
164
+ source_code: @example&.source
165
+ }
166
+ elsif exception.is_a?(ActionView::Template::Error) & exception.message.include?("implements a reserved method")
167
+ message_parts = exception.message.split("\n").first.split
168
+ component_class = message_parts.first.constantize
169
+ naughty_method = message_parts.last.delete("#").delete("`").delete(".")
170
+ p naughty_method
171
+ method = component_class.instance_method(naughty_method.to_sym)
172
+ if method
173
+ {
174
+ file_path: method.source_location.first,
175
+ line_number: method.source_location[1]
176
+ }
177
+ end
178
+ end
179
+ Lookbook::Error.new(exception, error_params || {})
157
180
  end
158
181
  end
159
182
  end
@@ -9,6 +9,10 @@ module Lookbook
9
9
  render_component "icon", name: name, size: size, **attrs
10
10
  end
11
11
 
12
+ def code(language = "ruby", **opts, &block)
13
+ render_component "code", {language: language, **opts}, &block
14
+ end
15
+
12
16
  if Rails.version.to_f < 6.1
13
17
  def class_names(*args)
14
18
  tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
@@ -7,10 +7,6 @@ module Lookbook
7
7
  lookbook.page_path page.lookup_path
8
8
  end
9
9
 
10
- def code(language = "ruby", line_numbers: false, &block)
11
- render_component "code", language: language, line_numbers: line_numbers, &block
12
- end
13
-
14
10
  def embed(*args, params: {}, type: :preview, **opts)
15
11
  return unless args.any?
16
12
 
@@ -1,13 +1,17 @@
1
1
  <%
2
- line_numbers ||= false
3
2
  language ||= "html"
4
3
  wrap ||= nil;
5
4
  %>
6
5
  <% code ||= capture do %><%= yield %><% end %>
7
- <div class="code not-prose <%= "numbered" if line_numbers %> <%= classes ||= "" %>"
6
+ <% output = highlight(code, language, {
7
+ strip: defined?(strip) ? strip : true,
8
+ line_numbers: line_numbers ||= false,
9
+ highlight_lines: highlight_lines ||= [],
10
+ start_line: start_line ||= 0
11
+ }) %>
12
+ <div class="code not-prose <%= "numbered" if line_numbers %> <%= classes ||= "" %> <%= "focussed" if highlight_lines.any? %>"
8
13
  x-data="code"
9
14
  :class="{'wrapped': wrap}"
10
- <% if wrap.present? %>x-effect="wrap = <%= wrap %>"<% end %>
11
- >
12
- <pre><code class="highlight"><%= highlight(code.strip, language, line_numbers: line_numbers) %></code></pre>
15
+ <% if wrap.present? %>x-effect="wrap = <%= wrap %>"<% end %>>
16
+ <pre><code class="highlight"><%= output %></code></pre>
13
17
  </div>
@@ -0,0 +1,13 @@
1
+ <div class="bg-red-50 w-full overflow-auto h-full">
2
+ <ul class="text-sm divide-y divide-red-200">
3
+ <% errors.each do |error| %>
4
+ <% error = error.is_a?(Lookbook::Error) ? error : Lookbook::Error.new(error) %>
5
+ <li class="px-4 py-3">
6
+ <h4 class="break-all leading-tight">
7
+ <%= error.file_name %><%= ":#{error.line_number}" if error.line_number %>
8
+ </h4>
9
+ <pre class="text-red-800 text-xs mt-2 whitespace-pre-wrap opacity-80 font-mono"><%= error.message %></pre>
10
+ </li>
11
+ <% end %>
12
+ </ul>
13
+ </div>
@@ -22,7 +22,7 @@
22
22
  </div>
23
23
  <div class="flex items-stretch h-full ml-auto space-x-3">
24
24
  <div
25
- class="flex items-center text-xs font-monospace text-gray-700 space-x-1 opacity-50 hover:opacity-100 transition"
25
+ class="flex items-center text-xs font-mono text-gray-700 space-x-1 opacity-50 hover:opacity-100 transition"
26
26
  :class="{'opacity-100': $store.inspector.preview.resizing}"
27
27
  x-show="isActivePreviewPanel('preview')">
28
28
  <span x-text="`${preview.width}px`"></span>
@@ -1,7 +1,7 @@
1
1
  <div
2
2
  x-data="sidebar"
3
3
  @page:morphed.window="setActiveNavItem"
4
- class="h-full bg-gray-100 overflow-hidden flex flex-col"
4
+ class="h-full bg-gray-100 overflow-hidden flex flex-col relative"
5
5
  x-show="$store.sidebar.open"
6
6
  x-cloak>
7
7
 
@@ -14,8 +14,8 @@
14
14
 
15
15
  <% if feature_enabled?(:pages) && Lookbook.pages.any? %>
16
16
  <div
17
- class="grid overflow-hidden"
18
- :style="`grid-template-rows: ${$store.sidebar.panelSplits[0] || 1}fr 1px ${$store.sidebar.panelSplits[1] || 1}fr; height: calc(100vh - 40px)`"
17
+ class="grid overflow-hidden flex-grow"
18
+ :style="`grid-template-rows: ${$store.sidebar.panelSplits[0] || 1}fr 1px ${$store.sidebar.panelSplits[1] || 1}fr;`"
19
19
  x-ref="sidebarPanels">
20
20
  <div class="flex flex-col overflow-hidden">
21
21
  <div class="flex items-center flex-none border-b border-gray-300 h-10 bg-white relative px-4">
@@ -52,4 +52,18 @@
52
52
  </div>
53
53
  <% end %>
54
54
 
55
+ <% if Lookbook::Preview.errors.any? %>
56
+ <div class="flex-none" x-ref="preview-errors" id="preview-errors">
57
+ <div class="flex items-center border-b border-t border-gray-300 h-10 bg-white px-4">
58
+ <h2 class="flex items-center flex-none">
59
+ <%= icon "alert-triangle", size: 4, class: "text-red-700" %>
60
+ <span class="ml-2">Preview load errors</span>
61
+ </h2>
62
+ </div>
63
+ <div class="h-full max-h-[300px] overflow-hidden">
64
+ <%= component "errors", errors: Lookbook::Preview.errors %>
65
+ </div>
66
+ </div>
67
+ <% end %>
68
+
55
69
  </div>
@@ -0,0 +1,46 @@
1
+ <%
2
+ error = error.is_a?(Lookbook::Error) ? error : Lookbook::Error.new(error)
3
+ @title = error.title
4
+ disable_header ||= false
5
+ %>
6
+ <%= component "header" unless disable_header %>
7
+ <div class="h-full w-full bg-red-50 flex flex-col border-red-300" id="error-<%= Time.now %>">
8
+
9
+ <header class="mx-8 pt-8 mb-8 flex-none">
10
+ <h2 class="text-xl font-bold text-red-700"><%= error.title %></h2>
11
+ </header>
12
+
13
+ <div class="flex-none px-8 py-6 mb-8 border-t border-b border-red-200 bg-red-100 text-base font-mono leading-relaxed">
14
+ <pre class="whitespace-pre-wrap font-sans leading-tight text-red-900"><%= error.message %></pre>
15
+ </div>
16
+
17
+ <% if error.file_name %>
18
+ <div class="text-gray-800 text-sm mx-8 mb-2 font-mono flex-none <%= "pl-2" if error.source_code %>">
19
+ <span><%= error.file_name %></span>
20
+ <% if error.line_number %>
21
+ <span>[line <strong><%= error.line_number %></strong>]</span>
22
+ <% end %>
23
+ </div>
24
+ <% end %>
25
+
26
+ <% if error.source_code %>
27
+ <div class="prose max-w-full mx-8">
28
+ <%= code error.file_lang, highlight_lines: [error.source_code[:highlighted_line]],
29
+ line_numbers: true,
30
+ start_line: error.source_code[:start_line],
31
+ strip: false,
32
+ class: "py-4 !max-w-full !border-red-200 !mt-1" do %><%= h(error.source_code[:code]) %><% end %>
33
+ </div>
34
+ <% end %>
35
+
36
+ <h3 class="font-bold mb-4 px-8 mt-8 flex-none">Full stack trace</h3>
37
+ <div class="text-xs font-mono flex-grow h-full overflow-hidden">
38
+ <div class="h-full overflow-auto px-8 pb-10 text-gray-400 leading-relaxed">
39
+ <% error.backtrace.each do |line| %>
40
+ <div class="hover:text-gray-900 transition-colors duration-100">
41
+ <%= line %>
42
+ </div>
43
+ <% end %>
44
+ </div>
45
+ </div>
46
+ </div>
@@ -5,7 +5,8 @@ module Lookbook
5
5
  module CodeFormatter
6
6
  class << self
7
7
  def highlight(source, language, opts = {})
8
- source&.gsub!("&gt;", "<")&.gsub!("&lt;", ">")
8
+ source&.strip! unless opts[:strip] == false
9
+ source&.gsub!("&gt;", ">")&.gsub!("&lt;", "<")
9
10
  language ||= "ruby"
10
11
  formatter = Formatter.new(opts)
11
12
  lexer = Rouge::Lexer.find(language.to_s) || Rouge::Lexer.find("plaintext")
@@ -23,12 +24,14 @@ module Lookbook
23
24
  class Formatter < Rouge::Formatters::HTML
24
25
  def initialize(opts = {})
25
26
  @opts = opts
27
+ @highlight_lines = opts[:highlight_lines].to_a || []
28
+ @start_line = opts[:start_line] || 0
26
29
  end
27
30
 
28
31
  def stream(tokens, &block)
29
32
  token_lines(tokens).each_with_index do |line_tokens, i|
30
- yield "<div class='line'>"
31
- yield "<span class='line-number'>#{i}</span>" if @opts[:line_numbers]
33
+ yield "<div class='line #{"highlighted-line" if @highlight_lines.include?(i + 1)}'>"
34
+ yield "<span class='line-number'>#{@start_line + i}</span>" if @opts[:line_numbers]
32
35
  yield "<span class='line-content'>"
33
36
  line_tokens.each do |token, value|
34
37
  yield span(token, value)
@@ -82,22 +82,21 @@ module Lookbook
82
82
  end
83
83
  end
84
84
 
85
- initializer "lookbook.helpers" do
86
- config.action_controller.include_all_helpers = false
87
- end
88
-
89
85
  config.after_initialize do
90
- Array(config.view_component.preview_paths).each do |preview_path|
91
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
92
- end
93
-
94
86
  @preview_listener = Listen.to(*config.lookbook.listen_paths, only: /\.(rb|html.*)$/) do |modified, added, removed|
95
- parser.parse
87
+ if Lookbook::Preview.errors.any?
88
+ Lookbook::Preview.reload
89
+ end
90
+ begin
91
+ parser.parse
92
+ rescue
93
+ end
96
94
  if Lookbook::Engine.websocket
97
- if (modified.any? || removed.any?) && added.none?
95
+ if modified.any? || removed.any? || added.none?
98
96
  Lookbook::Engine.websocket.broadcast("reload", {
99
97
  modified: modified,
100
- removed: removed
98
+ removed: removed,
99
+ added: added
101
100
  })
102
101
  end
103
102
  end
@@ -0,0 +1,121 @@
1
+ module Lookbook
2
+ class Error < StandardError
3
+ delegate :full_message, :backtrace, :to_s, to: :target
4
+
5
+ LINES_AROUND = 3
6
+
7
+ def initialize(original = nil, title: nil, message: nil, file_path: nil, file_name: nil, line_number: nil, source_code: nil)
8
+ @original = original
9
+ @title = title
10
+ @message = message
11
+ @file_path = file_path
12
+ @file_name = file_name
13
+ @line_number = line_number
14
+ @source_code = source_code
15
+ super()
16
+ end
17
+
18
+ def source_code
19
+ lines = source_code_lines
20
+
21
+ if lines.present? && line_number.is_a?(Integer)
22
+ start_line = source_code_start_line(lines)
23
+ end_line = source_code_end_line(lines)
24
+ highlighted_line = source_code_highlighted_line(lines)
25
+
26
+ line_count = end_line - start_line
27
+ relevant_lines = lines.slice(start_line - 1, line_count + 1)
28
+ if relevant_lines.present?
29
+ empty_start_lines = 0
30
+ relevant_lines.each do |line|
31
+ break unless line.strip.empty?
32
+ empty_start_lines += 1
33
+ end
34
+
35
+ {
36
+ code: relevant_lines.join("\n").lstrip,
37
+ start_line: start_line - empty_start_lines,
38
+ end_line: end_line - empty_start_lines,
39
+ highlighted_line: highlighted_line - empty_start_lines
40
+ }
41
+ end
42
+
43
+ end
44
+ end
45
+
46
+ def source_code_lines
47
+ if file_path || @source_code
48
+ if @source_code
49
+ @source_code.split("\n")
50
+ else
51
+ full_path = Rails.root.join(file_path)
52
+ File.read(full_path).split("\n") if File.exist? full_path
53
+ end
54
+ end
55
+ end
56
+
57
+ def file_lang
58
+ lang = Lookbook::Lang.guess(file_path)
59
+ lang.present? ? lang[:name] : "plaintext"
60
+ end
61
+
62
+ def title
63
+ @title || target.class.to_s
64
+ end
65
+
66
+ def message
67
+ (@message || target.message).gsub("(<unknown>):", "").strip.upcase_first
68
+ end
69
+
70
+ def file_name
71
+ if @file_name == false
72
+ nil
73
+ else
74
+ @file_name || file_path
75
+ end
76
+ end
77
+
78
+ def file_path
79
+ path = if @file_path.nil?
80
+ parsed_backtrace[0][0] if parsed_backtrace.any?
81
+ else
82
+ @file_path.presence || nil
83
+ end
84
+ path.nil? ? nil : path.to_s.delete_prefix("#{Rails.root}/")
85
+ end
86
+
87
+ def line_number
88
+ number = if @line_number.nil?
89
+ parsed_backtrace[0][1] if parsed_backtrace.any?
90
+ else
91
+ @line_number.presence || nil
92
+ end
93
+ number.present? ? number.to_i : number
94
+ end
95
+
96
+ def parsed_backtrace
97
+ backtrace.map do |x|
98
+ x =~ /^(.+?):(\d+)(|:in `(.+)')$/
99
+ [$1, $2, $4]
100
+ end
101
+ end
102
+
103
+ protected
104
+
105
+ def target
106
+ @original.presence || self
107
+ end
108
+
109
+ def source_code_start_line(lines)
110
+ [(line_number - LINES_AROUND), 1].max unless line_number.nil?
111
+ end
112
+
113
+ def source_code_end_line(lines)
114
+ [line_number + LINES_AROUND, lines&.size || Infinity].min
115
+ end
116
+
117
+ def source_code_highlighted_line(lines)
118
+ [line_number - source_code_start_line(lines) + 1, 1].max unless line_number.nil?
119
+ end
120
+ end
121
+ end
data/lib/lookbook/page.rb CHANGED
@@ -15,9 +15,13 @@ module Lookbook
15
15
  :data
16
16
  ]
17
17
 
18
+ attr_reader :errors
19
+
18
20
  def initialize(path, base_path)
19
21
  @pathname = Pathname.new path
20
22
  @base_path = base_path
23
+ @options = nil
24
+ @errors = []
21
25
  end
22
26
 
23
27
  def path
@@ -58,7 +62,7 @@ module Lookbook
58
62
  end
59
63
 
60
64
  def content
61
- @content ||= strip_frontmatter(file_contents)
65
+ @content ||= strip_frontmatter(file_contents).strip
62
66
  end
63
67
 
64
68
  def matchers
@@ -97,18 +101,28 @@ module Lookbook
97
101
 
98
102
  def options
99
103
  return @options if @options
100
- frontmatter = (get_frontmatter(file_contents) || {}).deep_symbolize_keys
101
- options = Lookbook.config.page_options.deep_merge(frontmatter).with_indifferent_access
102
- options[:id] = options[:id] ? generate_id(options[:id]) : generate_id(lookup_path)
103
- options[:label] ||= name.titleize
104
- options[:title] ||= options[:label]
105
- options[:hidden] ||= false
106
- options[:landing] ||= false
107
- options[:position] = options[:position] ? options[:position].to_i : get_position_prefix(path_name)
108
- options[:markdown] ||= markdown_file?
109
- options[:header] = true unless options.key? :header
110
- options[:footer] = true unless options.key? :footer
111
- @options ||= options
104
+ begin
105
+ frontmatter = (get_frontmatter(file_contents) || {}).deep_symbolize_keys
106
+ rescue => exception
107
+ frontmatter = {}
108
+ line_number_match = exception.message.match(/.*line\s(\d+)/)
109
+ @errors.push(Lookbook::Error.new(exception, {
110
+ title: "YAML frontmatter parsing error",
111
+ file_path: @pathname.to_s,
112
+ line_number: line_number_match ? line_number_match[1] : false
113
+ }))
114
+ end
115
+ @options = Lookbook.config.page_options.deep_merge(frontmatter).with_indifferent_access
116
+ @options[:id] = @options[:id] ? generate_id(@options[:id]) : generate_id(lookup_path)
117
+ @options[:label] ||= name.titleize
118
+ @options[:title] ||= @options[:label]
119
+ @options[:hidden] ||= false
120
+ @options[:landing] ||= false
121
+ @options[:position] = @options[:position] ? @options[:position].to_i : get_position_prefix(path_name)
122
+ @options[:markdown] ||= markdown_file?
123
+ @options[:header] = true unless @options.key? :header
124
+ @options[:footer] = true unless @options.key? :footer
125
+ @options
112
126
  end
113
127
 
114
128
  def path_name
@@ -34,7 +34,7 @@ module Lookbook
34
34
  return @examples if @examples.present?
35
35
  public_methods = @preview.public_instance_methods(false)
36
36
  public_method_objects = @preview_inspector&.methods&.filter { |m| public_methods.include?(m.name) }
37
- examples = public_method_objects&.map { |m| PreviewExample.new(m.name.to_s, self) }
37
+ examples = (public_method_objects || []).map { |m| PreviewExample.new(m.name.to_s, self) }
38
38
  sorted = Lookbook.config.sort_examples ? examples.sort_by(&:label) : examples
39
39
  @examples = []
40
40
  if @preview_inspector&.groups&.any?
@@ -49,7 +49,7 @@ module Lookbook
49
49
  else
50
50
  @examples = sorted
51
51
  end
52
- @examples
52
+ @examples = @examples.compact
53
53
  end
54
54
 
55
55
  def default_example
@@ -68,7 +68,7 @@ module Lookbook
68
68
  end
69
69
 
70
70
  def preview_paths
71
- ViewComponent::Preview.preview_paths
71
+ ViewComponent::Base.preview_paths
72
72
  end
73
73
 
74
74
  def parent_collections_names
@@ -88,19 +88,78 @@ module Lookbook
88
88
  end
89
89
 
90
90
  class << self
91
+ def find(path)
92
+ all.find { |p| p.lookup_path == path }
93
+ end
94
+
95
+ def exists?(path)
96
+ !!find(path)
97
+ end
98
+
91
99
  def all
92
- previews = ViewComponent::Preview.all.map { |p| new(p) }
100
+ previews = load_previews.map do |p|
101
+ new(p)
102
+ rescue
103
+ Rails.logger.error "[lookbook] error instantiating preview\n#{exception.full_message}"
104
+ end
93
105
 
94
- sorted_previews = previews.sort_by { |preview| [preview.position, preview.label] }
106
+ sorted_previews = previews.compact.sort_by { |preview| [preview.position, preview.label] }
95
107
  PreviewCollection.new(sorted_previews)
96
108
  end
97
109
 
98
- def find(path)
99
- all.find { |p| p.lookup_path == path }
110
+ def errors
111
+ @errors || []
100
112
  end
101
113
 
102
- def exists?(path)
103
- !!find(path)
114
+ def reload
115
+ load_previews
116
+ end
117
+
118
+ protected
119
+
120
+ def reset_files_data
121
+ @loaded_files = []
122
+ @errors = []
123
+ end
124
+
125
+ def load_previews
126
+ reset_files_data if @loaded_files.nil?
127
+ require_preview_files if @errors.any?
128
+
129
+ preview_classes = ViewComponent::Preview.descendants
130
+ if preview_files.size > preview_classes.size
131
+ require_preview_files
132
+ end
133
+
134
+ ViewComponent::Preview.descendants.filter { |klass| @loaded_files.include? "#{klass.name.underscore}.rb" }
135
+ end
136
+
137
+ def require_preview_files
138
+ reset_files_data
139
+ preview_files.each do |file|
140
+ require_dependency(file[:path])
141
+ @loaded_files.push(file[:rel_path])
142
+ rescue => exception
143
+ Rails.logger.error "[lookbook] preview error\n#{exception.full_message}\n"
144
+ @errors.push(Lookbook::Error.new(exception, {
145
+ title: "Preview #{exception.class}",
146
+ file_name: file[:rel_path],
147
+ file_path: file[:path]
148
+ }))
149
+ end
150
+ end
151
+
152
+ def preview_files
153
+ files = Array(Lookbook.config.preview_paths).map do |preview_path|
154
+ Dir["#{preview_path}/**/*_preview.rb"].map do |path|
155
+ {
156
+ path: path,
157
+ base_path: preview_path,
158
+ rel_path: Pathname(path).relative_path_from(preview_path).to_s
159
+ }
160
+ end
161
+ end
162
+ files.flatten
104
163
  end
105
164
  end
106
165