lookbook 0.7.1 → 0.7.2.beta.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46bd65cde7621bed3438857629f7d890a88fa39a44bd4c086b62aa72b8574a7e
4
- data.tar.gz: ccab74ddf91a4244b9bc24b847a412ac2f4442f07b2af65a720f1b3ad6b14d3e
3
+ metadata.gz: 990948c73a9d1c658a44b0ae4ac88c2c850fa92dfe9e40551b4634311113fe87
4
+ data.tar.gz: a203b8a19a1b9dc3d3ec91af757b1bb75edd84fed009e329f720b7e5891f737e
5
5
  SHA512:
6
- metadata.gz: 6e1bda8b0cd0116303f9474982366efbbecdd39fcca4bc43a352edb6477b2cb5720c9e0837eeabb6d6e1f1906b63be656d91e684af12b76eb82023b765ac1494
7
- data.tar.gz: 55d0b171d6e81b9cecb8952e293be88c6a412d7bb9c00617a0989297a89778f186f09744806701f9c191907a2d33bcc8cf68854932a5cfdf9d1d6011d3d1b00d
6
+ metadata.gz: bb588034ce795c97cccffbf1d7de75bbce28895009975576e8d2bcf50a3e4878771fe38cba600aa6986d59b3e3579023a2782a8dc0cc4bd349376a8bd67978b5
7
+ data.tar.gz: 3485c0882de17d7a3c63a27872a79615043d7c03651e555394f53accaed865c36b45e43f086deb23dd8d0a4118049ac4cbd936943765afb619e0a9e413287d7e
@@ -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)
@@ -78,22 +78,24 @@ module Lookbook
78
78
  config.lookbook.cable.logger ||= Rails.logger
79
79
  else
80
80
  config.lookbook.cable.logger = Lookbook::NullLogger.new
81
- config.action_view.logger = Lookbook::NullLogger.new
82
81
  end
83
82
  end
84
83
 
85
84
  config.after_initialize do
86
- Array(config.view_component.preview_paths).each do |preview_path|
87
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
88
- end
89
-
90
85
  @preview_listener = Listen.to(*config.lookbook.listen_paths, only: /\.(rb|html.*)$/) do |modified, added, removed|
91
- parser.parse
86
+ if Lookbook::Preview.errors.any?
87
+ Lookbook::Preview.reload
88
+ end
89
+ begin
90
+ parser.parse
91
+ rescue
92
+ end
92
93
  if Lookbook::Engine.websocket
93
- if (modified.any? || removed.any?) && added.none?
94
+ if modified.any? || removed.any? || added.none?
94
95
  Lookbook::Engine.websocket.broadcast("reload", {
95
96
  modified: modified,
96
- removed: removed
97
+ removed: removed,
98
+ added: added
97
99
  })
98
100
  end
99
101
  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