andromeda_cms 0.3.0 → 0.4.0
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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +43 -0
- data/lib/andromeda/build_context.rb +81 -0
- data/lib/andromeda/components/errors.rb +20 -1
- data/lib/andromeda/components/import_scanner.rb +2 -3
- data/lib/andromeda/components.rb +50 -1
- data/lib/andromeda/entry.rb +7 -2
- data/lib/andromeda/helpers.rb +11 -2
- data/lib/andromeda/pipeline.rb +50 -35
- data/lib/andromeda/version.rb +1 -1
- data/lib/andromeda_cms.rb +1 -0
- data/lib/generators/andromeda/component/component_generator.rb +8 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2702633c9baaedc1b40c9a7bf767fe41dd472276bd1f279cabcea893746b9924
|
|
4
|
+
data.tar.gz: 6c4c0463b1709aa434a11ce3700f67a4b2d573237353cef8f2ed87e034b8b742
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6dae30e5bcd69aeb8665d1991be26ec0705471d8fe592c703f0824441adfa0b40dd69de95b6480616fe49348aebc6127ba2051611dc19156f62ea66192b396e
|
|
7
|
+
data.tar.gz: 856b571be0856f01afe55c14661e5ac9f49eb1b67b7329e712ebc077fa413a2b961a95832ee38374fa768b688779cbbc4ce705ab3bddccc9008d7cab0374b41a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- An image imported in MDX (`import photo from './photo.png'`) can be passed
|
|
13
|
+
to a component as a prop (`<Figure src={photo} />`). It is published like
|
|
14
|
+
an `image` attribute, and the partial receives an `Andromeda::Image`.
|
|
15
|
+
- `rails g andromeda:component` writes a strict locals declaration that
|
|
16
|
+
includes `content: nil`, since `content` is passed even to a self-closing
|
|
17
|
+
tag.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A component that queries entries (`Content::Post.find(slug)`) during
|
|
22
|
+
`andromeda:build` in production reads source files instead of the
|
|
23
|
+
previous build's `_index.json`, so an entry not converted yet is found.
|
|
24
|
+
- `andromeda_image_url` called inside a component while converting returns
|
|
25
|
+
an asset marker instead of a URL without a digest, which 404'd in
|
|
26
|
+
production. `andromeda_content` leaves markers in place there too.
|
|
27
|
+
- A component that renders another entry's HTML during `andromeda:build`
|
|
28
|
+
converts that entry on the spot when it has not been converted yet,
|
|
29
|
+
instead of raising `Andromeda::BuildMissing`. Entries whose components
|
|
30
|
+
render each other fail with a "circular conversion" error.
|
|
31
|
+
- An `Andromeda::Error` raised inside a component partial (such as
|
|
32
|
+
`EntryNotFound` from `find`) is no longer wrapped in
|
|
33
|
+
`ActionView::Template::Error`, so the build lists it with the other
|
|
34
|
+
problems instead of aborting with a stack trace.
|
|
35
|
+
|
|
8
36
|
## [0.3.0] - 2026-09-29
|
|
9
37
|
|
|
10
38
|
### Added
|
|
@@ -93,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
93
121
|
- Generators: `andromeda:install`, `andromeda:collection`,
|
|
94
122
|
`andromeda:component` and `andromeda:import_astro`.
|
|
95
123
|
|
|
124
|
+
[0.4.0]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.4.0
|
|
96
125
|
[0.3.0]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.3.0
|
|
97
126
|
[0.2.0]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.2.0
|
|
98
127
|
[0.1.2]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.1.2
|
data/README.md
CHANGED
|
@@ -168,8 +168,33 @@ import Callout from 'content_components/callout';
|
|
|
168
168
|
|
|
169
169
|
Props arrive as locals (`camelCase` becomes `snake_case`), the tag's children
|
|
170
170
|
arrive as `content`, and `<Fragment slot="header">` becomes a `header` local.
|
|
171
|
+
`content` is passed even to a self-closing tag (`<Callout />`), so a partial
|
|
172
|
+
using strict locals must declare it:
|
|
173
|
+
|
|
174
|
+
```erb
|
|
175
|
+
<%# locals: (type: "note", title: nil, content: nil) %>
|
|
176
|
+
```
|
|
177
|
+
|
|
171
178
|
`bin/rails generate andromeda:component Callout type title` writes the stub.
|
|
172
179
|
|
|
180
|
+
A component can query other entries, e.g. a card linking to another post:
|
|
181
|
+
|
|
182
|
+
```erb
|
|
183
|
+
<%# app/views/content_components/_post_card.html.erb %>
|
|
184
|
+
<%# locals: (slug:, content: nil) %>
|
|
185
|
+
<% post = Content::Blog.find(slug) %>
|
|
186
|
+
<a href="<%= blog_path(post.id) %>">
|
|
187
|
+
<img src="<%= andromeda_image_url(post.cover_image) %>" alt="">
|
|
188
|
+
<%= post.title %>
|
|
189
|
+
</a>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
During `andromeda:build` such queries read the source files, so an entry not
|
|
193
|
+
converted yet is still found, and `andromeda_content(post)` converts it on
|
|
194
|
+
the spot. Component HTML is rendered once at build time
|
|
195
|
+
and stored, so `andromeda_image_url` emits a placeholder there that
|
|
196
|
+
`andromeda_content` turns into the digest-stamped URL when the page is served.
|
|
197
|
+
|
|
173
198
|
### Images
|
|
174
199
|
|
|
175
200
|
Put an image next to the entry and reference it relatively:
|
|
@@ -191,6 +216,24 @@ through the pipeline too; anything under `public/` or on another host is left
|
|
|
191
216
|
untouched. Declare a frontmatter image with `attribute :hero_image, :image`
|
|
192
217
|
and render it with `andromeda_image_url`.
|
|
193
218
|
|
|
219
|
+
To pass an image to a component, import it and use it as a prop, as in Astro:
|
|
220
|
+
|
|
221
|
+
```mdx
|
|
222
|
+
import photo from './photo.png';
|
|
223
|
+
|
|
224
|
+
<Figure src={photo} caption="The view from the top" />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The partial receives the same `Andromeda::Image` an `image` attribute holds:
|
|
228
|
+
|
|
229
|
+
```erb
|
|
230
|
+
<%# locals: (src:, caption: nil, content: nil) %>
|
|
231
|
+
<figure>
|
|
232
|
+
<img src="<%= andromeda_image_url(src) %>" class="w-full" alt="">
|
|
233
|
+
<figcaption><%= caption %></figcaption>
|
|
234
|
+
</figure>
|
|
235
|
+
```
|
|
236
|
+
|
|
194
237
|
## Development and deployment
|
|
195
238
|
|
|
196
239
|
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Andromeda
|
|
4
|
+
# What the current thread is doing on Andromeda's behalf, for code that
|
|
5
|
+
# runs *inside* a conversion but cannot be told so directly: a component
|
|
6
|
+
# partial rendered into an entry's HTML calls the same `Entry.find` and
|
|
7
|
+
# `andromeda_image_url` a request-time view does, yet both must behave
|
|
8
|
+
# differently while the output is being produced.
|
|
9
|
+
#
|
|
10
|
+
# Thread-local because a build or a development request converts on one
|
|
11
|
+
# thread, and another thread serving requests at the same time must keep
|
|
12
|
+
# seeing the built index.
|
|
13
|
+
module BuildContext
|
|
14
|
+
BUILD_KEY = :andromeda_build_source_entries
|
|
15
|
+
CONVERTING_KEY = :andromeda_converting
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Marks the block as an `andromeda:build`. Queries made during it read
|
|
20
|
+
# source files instead of `_index.json`, which is still the previous
|
|
21
|
+
# build's (or absent) until each collection finishes -- so a component
|
|
22
|
+
# that looks up another entry would otherwise miss one that has not been
|
|
23
|
+
# converted yet. Each collection is loaded once per build and shared.
|
|
24
|
+
#
|
|
25
|
+
# Nested calls reuse the outer build's cache.
|
|
26
|
+
def building
|
|
27
|
+
return yield if building?
|
|
28
|
+
|
|
29
|
+
Thread.current[BUILD_KEY] = {}
|
|
30
|
+
begin
|
|
31
|
+
yield
|
|
32
|
+
ensure
|
|
33
|
+
Thread.current[BUILD_KEY] = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def building?
|
|
38
|
+
!Thread.current[BUILD_KEY].nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param entry_class [Class] an Andromeda::Entry subclass.
|
|
42
|
+
# @return [Array<Andromeda::Entry>] the block's result, cached for the
|
|
43
|
+
# rest of the build.
|
|
44
|
+
def source_entries(entry_class)
|
|
45
|
+
cache = Thread.current[BUILD_KEY]
|
|
46
|
+
return yield if cache.nil?
|
|
47
|
+
|
|
48
|
+
cache[entry_class] ||= yield
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Marks the block as rendering an entry's body into stored HTML. Anything
|
|
52
|
+
# that would bake a final asset URL into it must emit a marker instead:
|
|
53
|
+
# the digest is only known once `assets:precompile` runs, after the build.
|
|
54
|
+
#
|
|
55
|
+
# A component can ask another entry for its HTML, which converts that
|
|
56
|
+
# entry inside this one; the stack of entries being converted is what
|
|
57
|
+
# stops two entries whose components show each other from recursing
|
|
58
|
+
# forever.
|
|
59
|
+
#
|
|
60
|
+
# @param label [String] identifies the entry, e.g. "blog/hello-world".
|
|
61
|
+
# @raise [Andromeda::Error] if `label` is already being converted.
|
|
62
|
+
def converting(label)
|
|
63
|
+
stack = Thread.current[CONVERTING_KEY] ||= []
|
|
64
|
+
if stack.include?(label)
|
|
65
|
+
raise Andromeda::Error, "circular conversion: #{[*stack.drop_while { |l| l != label }, label].join(' -> ')} " \
|
|
66
|
+
"-- a component renders the HTML of an entry that renders this one"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
stack.push(label)
|
|
70
|
+
begin
|
|
71
|
+
yield
|
|
72
|
+
ensure
|
|
73
|
+
stack.pop
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def converting?
|
|
78
|
+
!(Thread.current[CONVERTING_KEY] || []).empty?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -71,11 +71,30 @@ module Andromeda
|
|
|
71
71
|
location = [path, line].compact.join(":")
|
|
72
72
|
super(
|
|
73
73
|
"#{location}: unsupported MDX expression `{#{source}}` -- only literals, " \
|
|
74
|
-
"`frontmatter.x` references, and comments are supported"
|
|
74
|
+
"`frontmatter.x` references, imported images, and comments are supported"
|
|
75
75
|
)
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# Raised when a prop passes an imported image (`import photo from
|
|
80
|
+
# './photo.png'` then `src={photo}`) that cannot be published: the file
|
|
81
|
+
# is missing, lies outside the project, or was not imported by a
|
|
82
|
+
# relative path. Raising at build time keeps a typo from shipping as a
|
|
83
|
+
# 404.
|
|
84
|
+
class InvalidImageImportError < Andromeda::Error
|
|
85
|
+
attr_reader :name, :specifier, :path, :line
|
|
86
|
+
|
|
87
|
+
def initialize(name, specifier, reason, path:, line:)
|
|
88
|
+
@name = name
|
|
89
|
+
@specifier = specifier
|
|
90
|
+
@path = path
|
|
91
|
+
@line = line
|
|
92
|
+
|
|
93
|
+
location = [path, line].compact.join(":")
|
|
94
|
+
super("#{location}: image `#{name}` imported from #{specifier.inspect} #{reason}")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
79
98
|
# Raised when there is no Rails renderer to hand a partial to -- either
|
|
80
99
|
# `ApplicationController` is not defined (Rails is not loaded / not
|
|
81
100
|
# booted yet) and no `view:` was passed explicitly to
|
|
@@ -29,9 +29,8 @@ module Andromeda
|
|
|
29
29
|
# @param tree [Hash] an mdast root node.
|
|
30
30
|
# @return [Hash{String => String}] the name a component tag would use
|
|
31
31
|
# in the document, mapped to the raw import path/specifier. An
|
|
32
|
-
# `import cover from './cover.png'`
|
|
33
|
-
#
|
|
34
|
-
# later used as a component tag, and image imports never are.
|
|
32
|
+
# `import cover from './cover.png'` ends up here too, which is how
|
|
33
|
+
# Components resolves `{cover}` passed as a prop to an image.
|
|
35
34
|
def scan(tree)
|
|
36
35
|
imports = {}
|
|
37
36
|
|
data/lib/andromeda/components.rb
CHANGED
|
@@ -36,9 +36,14 @@ module Andromeda
|
|
|
36
36
|
# the original contract this class was written against, so it defaults
|
|
37
37
|
# to `{}` and callers that don't pass it simply get `nil` back for
|
|
38
38
|
# every `frontmatter.x` reference instead of an error.
|
|
39
|
-
|
|
39
|
+
# @param content_root [String, nil] the collection's base directory, which
|
|
40
|
+
# decides where an imported image is published (see
|
|
41
|
+
# `Andromeda::Assets.publish`). Defaults to the content file's own
|
|
42
|
+
# directory.
|
|
43
|
+
def initialize(tree:, path: nil, view: nil, frontmatter: {}, content_root: nil)
|
|
40
44
|
@tree = tree
|
|
41
45
|
@path = path
|
|
46
|
+
@content_root = content_root
|
|
42
47
|
@frontmatter = frontmatter || {}
|
|
43
48
|
@view = view || default_view
|
|
44
49
|
@imports = ImportScanner.scan(tree)
|
|
@@ -197,12 +202,48 @@ module Andromeda
|
|
|
197
202
|
end
|
|
198
203
|
|
|
199
204
|
def evaluate_prop_expression(source, node)
|
|
205
|
+
name = source.to_s.strip
|
|
206
|
+
return imported_image(name, node) if image_import?(name)
|
|
207
|
+
|
|
200
208
|
StaticExpression.evaluate(source, frontmatter: @frontmatter)
|
|
201
209
|
rescue StaticExpression::UnsupportedExpressionError
|
|
202
210
|
position = node_position(node)
|
|
203
211
|
raise UnsupportedExpressionError.new(source, path: @path, line: position[:line], column: position[:column])
|
|
204
212
|
end
|
|
205
213
|
|
|
214
|
+
IMAGE_EXTENSIONS = %w[.png .jpg .jpeg .gif .webp .avif .svg].freeze
|
|
215
|
+
IDENTIFIER_RE = /\A[A-Za-z_$][\w$]*\z/.freeze
|
|
216
|
+
|
|
217
|
+
# `{photo}` where `photo` was bound by `import photo from './photo.png'`
|
|
218
|
+
# -- Astro's way of passing an image to a component.
|
|
219
|
+
def image_import?(name)
|
|
220
|
+
return false unless IDENTIFIER_RE.match?(name)
|
|
221
|
+
|
|
222
|
+
specifier = @imports[name]
|
|
223
|
+
!specifier.nil? && IMAGE_EXTENSIONS.include?(File.extname(specifier).downcase)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Publishes the imported file the same way an `image` attribute is, so
|
|
227
|
+
# the partial receives an Andromeda::Image and `andromeda_image_url`
|
|
228
|
+
# works on it unchanged. Containment is checked before existence so a
|
|
229
|
+
# crafted path never learns whether something outside the project exists.
|
|
230
|
+
#
|
|
231
|
+
# @return [Andromeda::Image]
|
|
232
|
+
# @raise [InvalidImageImportError]
|
|
233
|
+
def imported_image(name, node)
|
|
234
|
+
specifier = @imports[name]
|
|
235
|
+
invalid = ->(reason) { InvalidImageImportError.new(name, specifier, reason, path: @path, line: node_position(node)[:line]) }
|
|
236
|
+
raise invalid.call("must be a relative path (./ or ../)") unless specifier.start_with?("./", "../") && @path
|
|
237
|
+
|
|
238
|
+
source = File.expand_path(specifier, File.dirname(File.expand_path(@path)))
|
|
239
|
+
content_root = @content_root || File.dirname(source)
|
|
240
|
+
raise invalid.call("resolves outside the project") unless Andromeda::Assets.logical_path(source, content_root: content_root)
|
|
241
|
+
raise invalid.call("does not exist") unless File.file?(source)
|
|
242
|
+
|
|
243
|
+
logical = Andromeda::Assets.publish(source, content_root: content_root)
|
|
244
|
+
Andromeda::Image.new(path: source, relative_path: specifier, asset: Andromeda::Assets.marker_for(logical))
|
|
245
|
+
end
|
|
246
|
+
|
|
206
247
|
# @return [Hash{Symbol => String}] `{content: ...}` plus one entry per
|
|
207
248
|
# named slot. When no child carries a `slot` attribute this
|
|
208
249
|
# is just `{content: children_html}` -- the common case reuses the
|
|
@@ -259,6 +300,14 @@ module Andromeda
|
|
|
259
300
|
@view.render(partial: partial, locals: locals).html_safe
|
|
260
301
|
rescue ActionView::MissingTemplate
|
|
261
302
|
raise missing_partial_error(name, node, partial)
|
|
303
|
+
rescue ActionView::Template::Error => e
|
|
304
|
+
# A partial that queries entries (`Content::Post.find(slug)`) can
|
|
305
|
+
# raise Andromeda's own errors; ActionView wraps them, which would
|
|
306
|
+
# slip past the build's `rescue Andromeda::Error` and abort it with a
|
|
307
|
+
# stack trace instead of listing the problem with the others.
|
|
308
|
+
raise e.cause if e.cause.is_a?(Andromeda::Error)
|
|
309
|
+
|
|
310
|
+
raise
|
|
262
311
|
ensure
|
|
263
312
|
restore_view_annotation(annotate_before)
|
|
264
313
|
end
|
data/lib/andromeda/entry.rb
CHANGED
|
@@ -170,7 +170,7 @@ module Andromeda
|
|
|
170
170
|
# @return [Array<Andromeda::Entry>]
|
|
171
171
|
# @raise [Andromeda::LoaderError] listing every file that failed.
|
|
172
172
|
def source_entries
|
|
173
|
-
load_entries_from_source
|
|
173
|
+
Andromeda::BuildContext.source_entries(self) { load_entries_from_source }
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
# Drops the in-memory cache, forcing the next query to re-run the
|
|
@@ -186,8 +186,13 @@ module Andromeda
|
|
|
186
186
|
# needs was validated and written to `_index.json` at build time, so
|
|
187
187
|
# reading it back is both faster and immune to a content file that
|
|
188
188
|
# changed on disk after the build. Development goes to the Loader so
|
|
189
|
-
# edits are visible without a build step.
|
|
189
|
+
# edits are visible without a build step. A build is the exception:
|
|
190
|
+
# the index is only rewritten once a collection finishes converting,
|
|
191
|
+
# so a component querying entries mid-build reads source instead
|
|
192
|
+
# (see Andromeda::BuildContext).
|
|
190
193
|
def entries
|
|
194
|
+
return source_entries if Andromeda::BuildContext.building?
|
|
195
|
+
|
|
191
196
|
@entries ||= production? ? entries_from_index : load_entries_from_source
|
|
192
197
|
end
|
|
193
198
|
|
data/lib/andromeda/helpers.rb
CHANGED
|
@@ -8,19 +8,28 @@ module Andromeda
|
|
|
8
8
|
# Outputs an entry's converted body. Marked html_safe because the build
|
|
9
9
|
# produced this markup from content the application's own authors wrote,
|
|
10
10
|
# the same trust model Astro applies to `.md`/`.mdx` (see 13_security).
|
|
11
|
+
# Inside a component rendered at conversion time the asset markers are
|
|
12
|
+
# left in place, for the same reason `andromeda_image_url` leaves them.
|
|
11
13
|
#
|
|
12
14
|
# @param entry [Andromeda::Entry]
|
|
13
15
|
# @return [ActiveSupport::SafeBuffer]
|
|
14
16
|
def andromeda_content(entry)
|
|
15
|
-
Andromeda::Assets.resolve(entry.html)
|
|
17
|
+
html = Andromeda::BuildContext.converting? ? entry.html : Andromeda::Assets.resolve(entry.html)
|
|
18
|
+
html.html_safe # rubocop:disable Rails/OutputSafety
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
# Inside a component partial rendered at conversion time, this returns
|
|
22
|
+
# the asset marker instead of a URL: the digest does not exist until
|
|
23
|
+
# `assets:precompile` runs, after the build, and `andromeda_content`
|
|
24
|
+
# resolves the marker when the page is served.
|
|
25
|
+
#
|
|
18
26
|
# @param image [Andromeda::Image, nil] an `image` attribute's value.
|
|
19
27
|
# @return [String, nil] its URL, or nil when the attribute is unset.
|
|
20
28
|
def andromeda_image_url(image)
|
|
21
29
|
return nil if image.nil?
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
source = image.asset || image.relative_path
|
|
32
|
+
Andromeda::BuildContext.converting? ? source : Andromeda::Assets.resolve(source)
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
# Renders a nested table of contents from `entry.headings`.
|
data/lib/andromeda/pipeline.rb
CHANGED
|
@@ -62,10 +62,12 @@ module Andromeda
|
|
|
62
62
|
def convert(entry)
|
|
63
63
|
mdx = Andromeda::Parser.mdx?(entry.file_path)
|
|
64
64
|
tree = Andromeda::Parser.parse(entry.body, mdx: mdx, path: entry.file_path)
|
|
65
|
-
result = Andromeda::
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
result = Andromeda::BuildContext.converting("#{entry.collection}/#{entry.id}") do
|
|
66
|
+
Andromeda::Renderer.new(
|
|
67
|
+
components: components_for(entry, tree),
|
|
68
|
+
image_resolver: image_resolver_for(entry)
|
|
69
|
+
).render(tree)
|
|
70
|
+
end
|
|
69
71
|
|
|
70
72
|
payload = {
|
|
71
73
|
id: entry.id,
|
|
@@ -105,7 +107,11 @@ module Andromeda
|
|
|
105
107
|
def fetch(entry)
|
|
106
108
|
stored = @store.read_entry(entry.collection, entry.id)
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
# During a build a component may ask for an entry that has not been
|
|
111
|
+
# converted yet (or only by the previous build), so it is converted
|
|
112
|
+
# the development way. Once done, the digest and render key match and
|
|
113
|
+
# a second request reuses it.
|
|
114
|
+
if production? && !Andromeda::BuildContext.building?
|
|
109
115
|
return stored if stored
|
|
110
116
|
|
|
111
117
|
raise Andromeda::BuildMissing.new(collection: entry.collection, id: entry.id, file_path: entry.file_path)
|
|
@@ -177,28 +183,7 @@ module Andromeda
|
|
|
177
183
|
# @param entry_class [Class]
|
|
178
184
|
# @return [BuildResult]
|
|
179
185
|
def build_collection(entry_class)
|
|
180
|
-
|
|
181
|
-
begin
|
|
182
|
-
entry_class.source_entries
|
|
183
|
-
rescue Andromeda::LoaderError => e
|
|
184
|
-
return BuildResult.new(
|
|
185
|
-
collection: entry_class.collection_name, converted: 0,
|
|
186
|
-
errors: e.messages.map { |message| "[#{entry_class.collection_name}] #{message}" }
|
|
187
|
-
)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
errors = []
|
|
191
|
-
payloads = entries.filter_map do |entry|
|
|
192
|
-
begin
|
|
193
|
-
convert(entry)
|
|
194
|
-
rescue Andromeda::Error => e
|
|
195
|
-
errors << "[#{entry_class.collection_name}] #{entry.file_path}: #{e.message}"
|
|
196
|
-
nil
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
@store.replace_collection!(entry_class.collection_name, payloads)
|
|
201
|
-
BuildResult.new(collection: entry_class.collection_name, converted: payloads.size, errors: errors, entries: entries)
|
|
186
|
+
Andromeda::BuildContext.building { build_collection_from_source(entry_class) }
|
|
202
187
|
end
|
|
203
188
|
|
|
204
189
|
# Converts every registered collection, reporting every problem found
|
|
@@ -216,13 +201,15 @@ module Andromeda
|
|
|
216
201
|
# every collection, if any entry failed to convert or references an
|
|
217
202
|
# entry that does not exist.
|
|
218
203
|
def self.build_all(entry_classes = default_entry_classes, store: Andromeda::Store.new)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
204
|
+
Andromeda::BuildContext.building do
|
|
205
|
+
results = entry_classes.map { |entry_class| new(store: store).build_collection(entry_class) }
|
|
206
|
+
errors = results.flat_map(&:errors)
|
|
207
|
+
loaded = entry_classes.zip(results).filter_map { |entry_class, result| [entry_class, result.entries] if result.entries }
|
|
208
|
+
errors.concat(Andromeda::References.problems(loaded.to_h))
|
|
209
|
+
raise Andromeda::BuildError, errors unless errors.empty?
|
|
210
|
+
|
|
211
|
+
results
|
|
212
|
+
end
|
|
226
213
|
end
|
|
227
214
|
|
|
228
215
|
def self.default_entry_classes
|
|
@@ -231,6 +218,31 @@ module Andromeda
|
|
|
231
218
|
|
|
232
219
|
private
|
|
233
220
|
|
|
221
|
+
def build_collection_from_source(entry_class)
|
|
222
|
+
entries =
|
|
223
|
+
begin
|
|
224
|
+
entry_class.source_entries
|
|
225
|
+
rescue Andromeda::LoaderError => e
|
|
226
|
+
return BuildResult.new(
|
|
227
|
+
collection: entry_class.collection_name, converted: 0,
|
|
228
|
+
errors: e.messages.map { |message| "[#{entry_class.collection_name}] #{message}" }
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
errors = []
|
|
233
|
+
payloads = entries.filter_map do |entry|
|
|
234
|
+
begin
|
|
235
|
+
convert(entry)
|
|
236
|
+
rescue Andromeda::Error => e
|
|
237
|
+
errors << "[#{entry_class.collection_name}] #{entry.file_path}: #{e.message}"
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@store.replace_collection!(entry_class.collection_name, payloads)
|
|
243
|
+
BuildResult.new(collection: entry_class.collection_name, converted: payloads.size, errors: errors, entries: entries)
|
|
244
|
+
end
|
|
245
|
+
|
|
234
246
|
# What the stored HTML depends on besides the entry's own source.
|
|
235
247
|
# Component partials are rendered into the HTML at conversion time, so
|
|
236
248
|
# without this an edited partial would keep serving the old markup until
|
|
@@ -318,7 +330,10 @@ module Andromeda
|
|
|
318
330
|
return nil unless Andromeda::Parser.mdx?(entry.file_path)
|
|
319
331
|
return nil unless defined?(Andromeda::Components)
|
|
320
332
|
|
|
321
|
-
Andromeda::Components.new(
|
|
333
|
+
Andromeda::Components.new(
|
|
334
|
+
tree: tree, path: entry.file_path, view: nil, frontmatter: entry.data,
|
|
335
|
+
content_root: Andromeda::Registry[entry.collection]&.base_dir
|
|
336
|
+
)
|
|
322
337
|
end
|
|
323
338
|
end
|
|
324
339
|
end
|
data/lib/andromeda/version.rb
CHANGED
data/lib/andromeda_cms.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "andromeda/version"
|
|
4
4
|
require_relative "andromeda/errors"
|
|
5
5
|
require_relative "andromeda/configuration"
|
|
6
|
+
require_relative "andromeda/build_context"
|
|
6
7
|
require_relative "andromeda/assets"
|
|
7
8
|
require_relative "andromeda/helpers"
|
|
8
9
|
require_relative "andromeda/fix"
|
|
@@ -44,12 +44,20 @@ module Andromeda
|
|
|
44
44
|
def partial_content
|
|
45
45
|
<<~ERB
|
|
46
46
|
#{usage_comment}
|
|
47
|
+
#{strict_locals}
|
|
47
48
|
<div class="#{underscored_name.dasherize}">
|
|
48
49
|
#{prop_lines.join("\n")}#{"\n" unless prop_lines.empty?} <%= content %>
|
|
49
50
|
</div>
|
|
50
51
|
ERB
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
# Every prop is optional, and `content` is declared too: it is passed
|
|
55
|
+
# even to a self-closing tag (`<Callout />`), where it is empty.
|
|
56
|
+
def strict_locals
|
|
57
|
+
locals = props.map { |prop| "#{prop.underscore}: nil" } << "content: nil"
|
|
58
|
+
"<%# locals: (#{locals.join(', ')}) %>"
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
def prop_lines
|
|
54
62
|
props.map do |prop|
|
|
55
63
|
local = prop.underscore
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: andromeda_cms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LIMHAUS Inc.
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- ext/andromeda_cms/src/lib.rs
|
|
90
90
|
- ext/andromeda_cms/src/nodes.rs
|
|
91
91
|
- lib/andromeda/assets.rb
|
|
92
|
+
- lib/andromeda/build_context.rb
|
|
92
93
|
- lib/andromeda/check.rb
|
|
93
94
|
- lib/andromeda/components.rb
|
|
94
95
|
- lib/andromeda/components/errors.rb
|