andromeda_cms 0.1.2 → 0.2.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 +37 -0
- data/README.md +2 -0
- data/lib/andromeda/assets.rb +22 -0
- data/lib/andromeda/check.rb +3 -1
- data/lib/andromeda/entry.rb +11 -1
- data/lib/andromeda/pipeline.rb +30 -6
- data/lib/andromeda/references.rb +72 -0
- data/lib/andromeda/schema.rb +9 -1
- data/lib/andromeda/version.rb +1 -1
- data/lib/andromeda_cms.rb +1 -0
- data/lib/generators/andromeda/install/install_generator.rb +5 -1
- 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: fa08db7b74aca56bfdc4d9f07e16e0c0bea58955ee94b1ec949503c1a99450b9
|
|
4
|
+
data.tar.gz: e0f58387fd0f941d04287bcad45bad36d1cac7e4959e4c7cb37d242d06aeb0d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a560a7bfec8de8698fb1a3aee14443f4d7ed801944dc3552f98c027bbedc90746c7f151a639fc863e085db43eadce21357f59fca070f21a52ec7efabaa788dbb
|
|
7
|
+
data.tar.gz: 06f7ab5e0887284207fee9c51f083acc8abbb8ea488ad1e9d6dfab3df737fa97d11c5c06a094023f5759efbebc3ccb463b80a8097e939ed935a428a9b5f315e9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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.2.0] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The reader for an `:array` attribute declared `of: :reference` now resolves
|
|
13
|
+
each element to its entry, the way a single `:reference` reader already
|
|
14
|
+
did: `post.tags` returns the tag entries, in frontmatter order, and raises
|
|
15
|
+
`Andromeda::EntryNotFound` for an id with no entry. It used to return the
|
|
16
|
+
`Andromeda::Reference` pointers, which are still in `post.data[:tags]`.
|
|
17
|
+
- `andromeda:build` and `andromeda:check` now fail on content that used to
|
|
18
|
+
pass and then broke when served. Run `andromeda:check` before upgrading a
|
|
19
|
+
deploy to see what needs fixing:
|
|
20
|
+
- a `:reference`, alone or in an array, whose id has no entry in the target
|
|
21
|
+
collection, or that names a collection nobody registered;
|
|
22
|
+
- an image in the body written as a bare path (`blog/posts/x.png`) that
|
|
23
|
+
Propshaft's load path cannot find. These used to fall back to a plain
|
|
24
|
+
`/assets/...` URL and 404 in production. Without Propshaft the path is
|
|
25
|
+
still used as written.
|
|
26
|
+
- Declaring `:array` with `of: :reference` and no `collection:`, or with
|
|
27
|
+
`of: :enum` and no `values:`, now raises `ArgumentError` at definition time.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- An `:array` of `:reference` built its elements without the declared
|
|
32
|
+
`collection:`, so each `Andromeda::Reference` had a `nil` collection. The
|
|
33
|
+
first process worked; the next one to read `.andromeda/` failed with
|
|
34
|
+
`NoMethodError: undefined method 'to_sym' for nil`. `of: :enum` lost its
|
|
35
|
+
`values:` the same way and raised `NoMethodError` on any value.
|
|
36
|
+
- An image in the body written as `./` or `../` with no file behind it was
|
|
37
|
+
treated as an asset pipeline path and rendered as a broken `/assets/./...`
|
|
38
|
+
URL. It now fails the build, as the documentation says, and so does a
|
|
39
|
+
relative path that leaves the project.
|
|
40
|
+
- `andromeda:install` appended its ignore lines to the last line of a
|
|
41
|
+
`.gitignore` or `.dockerignore` that did not end with a newline, breaking
|
|
42
|
+
both that entry and `/.andromeda/`.
|
|
43
|
+
|
|
8
44
|
## [0.1.2] - 2026-09-27
|
|
9
45
|
|
|
10
46
|
### Fixed
|
|
@@ -49,6 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
49
85
|
- Generators: `andromeda:install`, `andromeda:collection`,
|
|
50
86
|
`andromeda:component` and `andromeda:import_astro`.
|
|
51
87
|
|
|
88
|
+
[0.2.0]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.2.0
|
|
52
89
|
[0.1.2]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.1.2
|
|
53
90
|
[0.1.1]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.1.1
|
|
54
91
|
[0.1.0]: https://github.com/AndromedaCMS/andromeda_cms/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Astro-compatible content collections for Ruby on Rails.
|
|
4
4
|
|
|
5
|
+
https://github.com/user-attachments/assets/7bde3b4f-7bdb-49f9-9646-cd981846a451
|
|
6
|
+
|
|
5
7
|
[Documentation](https://www.andromedacms.dev/docs) · [Changelog](CHANGELOG.md)
|
|
6
8
|
|
|
7
9
|
Copy the `.md` and `.mdx` files from an [Astro](https://astro.build) project
|
data/lib/andromeda/assets.rb
CHANGED
|
@@ -72,6 +72,28 @@ module Andromeda
|
|
|
72
72
|
File.join("/assets", logical_path)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Whether the asset pipeline can serve `logical_path`. `url_for` falls
|
|
76
|
+
# back to a plain /assets path for anything it cannot find, so without
|
|
77
|
+
# this check a typo only shows up as a 404 in production.
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean, nil] nil when there is no Propshaft load path to ask
|
|
80
|
+
# (no asset pipeline, or Sprockets), so callers can skip the check.
|
|
81
|
+
def exists?(logical_path)
|
|
82
|
+
load_path = pipeline_load_path
|
|
83
|
+
return nil if load_path.nil?
|
|
84
|
+
|
|
85
|
+
!load_path.find(logical_path).nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pipeline_load_path
|
|
89
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
90
|
+
|
|
91
|
+
assets = ::Rails.application.assets
|
|
92
|
+
assets.respond_to?(:load_path) && assets.load_path.respond_to?(:find) ? assets.load_path : nil
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
def logical_path(source, content_root:)
|
|
76
98
|
source = File.expand_path(source)
|
|
77
99
|
root = File.expand_path(content_root)
|
data/lib/andromeda/check.rb
CHANGED
|
@@ -15,18 +15,20 @@ module Andromeda
|
|
|
15
15
|
# @return [Array<String>] human-readable problems; empty when content is healthy.
|
|
16
16
|
def run(entry_classes = Andromeda::Pipeline.default_entry_classes)
|
|
17
17
|
problems = []
|
|
18
|
+
loaded = {}
|
|
18
19
|
|
|
19
20
|
entry_classes.each do |entry_class|
|
|
20
21
|
entries = load_entries(entry_class, problems)
|
|
21
22
|
next if entries.nil?
|
|
22
23
|
|
|
24
|
+
loaded[entry_class] = entries
|
|
23
25
|
entries.each do |entry|
|
|
24
26
|
problems.concat(frontmatter_style_problems(entry))
|
|
25
27
|
problems.concat(conversion_problems(entry))
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
problems
|
|
31
|
+
problems.concat(Andromeda::References.problems(loaded))
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def load_entries(entry_class, problems)
|
data/lib/andromeda/entry.rb
CHANGED
|
@@ -58,7 +58,8 @@ module Andromeda
|
|
|
58
58
|
# stored `Andromeda::Reference` to the actual target entry (lazily,
|
|
59
59
|
# via the registry) rather than returning the pointer itself --
|
|
60
60
|
# `post.data[:author]` still holds the raw Reference for callers that
|
|
61
|
-
# want it.
|
|
61
|
+
# want it. An `:array` of `:reference` resolves each element the same
|
|
62
|
+
# way, so `post.tags` returns the tag entries.
|
|
62
63
|
#
|
|
63
64
|
# @see Andromeda::Schema#attribute for the full parameter list.
|
|
64
65
|
def attribute(name, type, **options)
|
|
@@ -67,6 +68,8 @@ module Andromeda
|
|
|
67
68
|
|
|
68
69
|
if type == :reference
|
|
69
70
|
define_method(attr_name) { resolve_reference(attr_name) }
|
|
71
|
+
elsif type == :array && options[:of] == :reference
|
|
72
|
+
define_method(attr_name) { resolve_references(attr_name) }
|
|
70
73
|
else
|
|
71
74
|
define_method(attr_name) { data[attr_name] }
|
|
72
75
|
end
|
|
@@ -313,5 +316,12 @@ module Andromeda
|
|
|
313
316
|
# referencing an entry never forces every other collection to load.
|
|
314
317
|
Andromeda.get_entry(reference.collection, reference.id)
|
|
315
318
|
end
|
|
319
|
+
|
|
320
|
+
# @param attr_name [Symbol] an `:array` attribute declared `of: :reference`.
|
|
321
|
+
# @return [Array<Andromeda::Entry>] in the order the frontmatter lists them.
|
|
322
|
+
# @raise [Andromeda::EntryNotFound] for the first id with no entry.
|
|
323
|
+
def resolve_references(attr_name)
|
|
324
|
+
Array(data[attr_name]).map { |reference| Andromeda.get_entry(reference.collection, reference.id) }
|
|
325
|
+
end
|
|
316
326
|
end
|
|
317
327
|
end
|
data/lib/andromeda/pipeline.rb
CHANGED
|
@@ -22,7 +22,11 @@ module Andromeda
|
|
|
22
22
|
# components, syntax errors) -- collected rather than raised
|
|
23
23
|
# individually, so a build reports everything in one pass (same
|
|
24
24
|
# principle as Andromeda::LoaderError).
|
|
25
|
-
|
|
25
|
+
#
|
|
26
|
+
# `entries` holds what was read from source (nil when the collection
|
|
27
|
+
# failed to load), so `.build_all` can check references across
|
|
28
|
+
# collections without reading every file a second time.
|
|
29
|
+
BuildResult = Struct.new(:collection, :converted, :errors, :entries, keyword_init: true) do
|
|
26
30
|
def success? = errors.empty?
|
|
27
31
|
end
|
|
28
32
|
|
|
@@ -194,7 +198,7 @@ module Andromeda
|
|
|
194
198
|
end
|
|
195
199
|
|
|
196
200
|
@store.replace_collection!(entry_class.collection_name, payloads)
|
|
197
|
-
BuildResult.new(collection: entry_class.collection_name, converted: payloads.size, errors: errors)
|
|
201
|
+
BuildResult.new(collection: entry_class.collection_name, converted: payloads.size, errors: errors, entries: entries)
|
|
198
202
|
end
|
|
199
203
|
|
|
200
204
|
# Converts every registered collection, reporting every problem found
|
|
@@ -209,10 +213,13 @@ module Andromeda
|
|
|
209
213
|
# `Andromeda::Store.new`'s own `Andromeda.config.build_path` default.
|
|
210
214
|
# @return [Array<BuildResult>] one per collection, on success.
|
|
211
215
|
# @raise [Andromeda::BuildError] listing every problem found, across
|
|
212
|
-
# every collection, if any entry failed to convert
|
|
216
|
+
# every collection, if any entry failed to convert or references an
|
|
217
|
+
# entry that does not exist.
|
|
213
218
|
def self.build_all(entry_classes = default_entry_classes, store: Andromeda::Store.new)
|
|
214
219
|
results = entry_classes.map { |entry_class| new(store: store).build_collection(entry_class) }
|
|
215
220
|
errors = results.flat_map(&:errors)
|
|
221
|
+
loaded = entry_classes.zip(results).filter_map { |entry_class, result| [entry_class, result.entries] if result.entries }
|
|
222
|
+
errors.concat(Andromeda::References.problems(loaded.to_h))
|
|
216
223
|
raise Andromeda::BuildError, errors unless errors.empty?
|
|
217
224
|
|
|
218
225
|
results
|
|
@@ -270,7 +277,7 @@ module Andromeda
|
|
|
270
277
|
content_root = File.dirname(entry.file_path)
|
|
271
278
|
base_dir = Andromeda::Registry[entry.collection]&.base_dir || content_root
|
|
272
279
|
|
|
273
|
-
lambda do |url,
|
|
280
|
+
lambda do |url, node|
|
|
274
281
|
# Absolute URLs point at something already being served -- a file in
|
|
275
282
|
# `public/`, another host, or an inline data URI -- so they are left
|
|
276
283
|
# exactly as the author wrote them.
|
|
@@ -282,10 +289,27 @@ module Andromeda
|
|
|
282
289
|
next Andromeda::Assets.marker_for(logical) if logical
|
|
283
290
|
end
|
|
284
291
|
|
|
285
|
-
#
|
|
292
|
+
# `./` and `../` can only mean "relative to this file", so falling
|
|
293
|
+
# back to the asset pipeline would just turn a typo into a silent 404.
|
|
294
|
+
# Containment is checked first so a crafted path never learns whether
|
|
295
|
+
# something outside the project exists.
|
|
296
|
+
start = (node && node[:position] || {})[:start] || {}
|
|
297
|
+
location = start[:line] ? "#{start[:line]}:#{start[:column]}: " : ""
|
|
298
|
+
if url.start_with?("./", "../")
|
|
299
|
+
inside = Andromeda::Assets.logical_path(source, content_root: base_dir)
|
|
300
|
+
reason = inside ? "does not exist" : "resolves outside the project"
|
|
301
|
+
raise Andromeda::Error, "#{location}image #{url.inspect} #{reason}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# A bare path is not a file next to the content: treat it as a logical asset path so
|
|
286
305
|
# images the application already ships (`app/assets/images/logo.png`,
|
|
287
306
|
# written as `logo.png`) resolve through the asset pipeline and get
|
|
288
|
-
# their digest, instead of 404ing in production.
|
|
307
|
+
# their digest, instead of 404ing in production. Without Propshaft
|
|
308
|
+
# there is nothing to ask, so the path is trusted as written.
|
|
309
|
+
if Andromeda::Assets.exists?(url) == false
|
|
310
|
+
raise Andromeda::Error, "#{location}image #{url.inspect} is not in the asset pipeline"
|
|
311
|
+
end
|
|
312
|
+
|
|
289
313
|
Andromeda::Assets.marker_for(url)
|
|
290
314
|
end
|
|
291
315
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Andromeda
|
|
6
|
+
# Checks that every `:reference` attribute -- on its own or as the
|
|
7
|
+
# elements of an `:array` -- names an entry that actually exists.
|
|
8
|
+
#
|
|
9
|
+
# Entries resolve references lazily (Entry#resolve_reference), so without
|
|
10
|
+
# this a typo in an id only surfaces when a page asks for the target. It
|
|
11
|
+
# needs every target collection loaded, which is why only the whole-site
|
|
12
|
+
# build (Pipeline.build_all) and Andromeda::Check run it.
|
|
13
|
+
module References
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param entries_by_class [Hash{Class => Array<Andromeda::Entry>}]
|
|
17
|
+
# entries already loaded, reused as reference targets. A target
|
|
18
|
+
# collection missing from here is loaded from source.
|
|
19
|
+
# @return [Array<String>] human-readable problems; empty when every
|
|
20
|
+
# reference resolves.
|
|
21
|
+
def problems(entries_by_class)
|
|
22
|
+
ids = Hash.new { |cache, name| cache[name] = ids_for(name, entries_by_class) }
|
|
23
|
+
|
|
24
|
+
entries_by_class.flat_map do |entry_class, entries|
|
|
25
|
+
attributes = reference_attributes(entry_class)
|
|
26
|
+
next [] if attributes.empty?
|
|
27
|
+
|
|
28
|
+
entries.flat_map do |entry|
|
|
29
|
+
attributes.flat_map do |attr|
|
|
30
|
+
references_in(entry.data[attr.name]).filter_map do |reference|
|
|
31
|
+
problem_for(reference, ids[reference.collection])&.then do |message|
|
|
32
|
+
"[#{entry_class.collection_name}] #{entry.file_path}: #{attr.name}: #{message}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reference_attributes(entry_class)
|
|
41
|
+
entry_class.schema.attributes.values.select do |attr|
|
|
42
|
+
attr.type == :reference || (attr.type == :array && attr.of == :reference)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `Array(value)` would splat a single Reference (a Struct) into its
|
|
47
|
+
# fields, so the two shapes are told apart explicitly.
|
|
48
|
+
def references_in(value)
|
|
49
|
+
(value.is_a?(Array) ? value : [value]).grep(Andromeda::Reference)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def problem_for(reference, known)
|
|
53
|
+
return "unknown collection #{reference.collection.inspect}" if known == :unknown
|
|
54
|
+
# The target collection failed to load; that failure is reported on
|
|
55
|
+
# its own, and every reference into it would only repeat it.
|
|
56
|
+
return nil if known.nil?
|
|
57
|
+
return nil if known.include?(reference.id.to_s)
|
|
58
|
+
|
|
59
|
+
"no #{reference.collection.inspect} entry with id #{reference.id.to_s.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ids_for(name, entries_by_class)
|
|
63
|
+
entry_class = Andromeda::Registry[name]
|
|
64
|
+
return :unknown if entry_class.nil?
|
|
65
|
+
|
|
66
|
+
entries = entries_by_class.fetch(entry_class) { entry_class.source_entries }
|
|
67
|
+
entries.map(&:id).to_set
|
|
68
|
+
rescue Andromeda::LoaderError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/andromeda/schema.rb
CHANGED
|
@@ -108,6 +108,12 @@ module Andromeda
|
|
|
108
108
|
end
|
|
109
109
|
raise ArgumentError, "#{name}: :enum requires values:" if type == :enum && (values.nil? || values.empty?)
|
|
110
110
|
raise ArgumentError, "#{name}: :reference requires collection:" if type == :reference && collection.nil?
|
|
111
|
+
if type == :array && of == :reference && collection.nil?
|
|
112
|
+
raise ArgumentError, "#{name}: :array of :reference requires collection:"
|
|
113
|
+
end
|
|
114
|
+
if type == :array && of == :enum && (values.nil? || values.empty?)
|
|
115
|
+
raise ArgumentError, "#{name}: :array of :enum requires values:"
|
|
116
|
+
end
|
|
111
117
|
if type == :array && of && !TYPES.include?(of)
|
|
112
118
|
raise ArgumentError, "#{name}: unknown element type #{of.inspect} for of:"
|
|
113
119
|
end
|
|
@@ -291,7 +297,9 @@ module Andromeda
|
|
|
291
297
|
return [nil, "#{value.inspect} is not an array"] unless value.is_a?(Array)
|
|
292
298
|
return [value, nil] unless attr.of
|
|
293
299
|
|
|
294
|
-
|
|
300
|
+
# `values:` and `collection:` describe the elements, not the array, so
|
|
301
|
+
# they have to travel with the element type.
|
|
302
|
+
element_attr = Attribute.new(name: attr.name, type: attr.of, values: attr.values, collection: attr.collection)
|
|
295
303
|
errors = []
|
|
296
304
|
elements = value.each_with_index.map do |element, index|
|
|
297
305
|
coerced, error = coerce(element_attr, element, entry_path: entry_path, content_root: content_root)
|
data/lib/andromeda/version.rb
CHANGED
data/lib/andromeda_cms.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "andromeda/loader"
|
|
|
19
19
|
require_relative "andromeda/entry"
|
|
20
20
|
require_relative "andromeda/store"
|
|
21
21
|
require_relative "andromeda/pipeline"
|
|
22
|
+
require_relative "andromeda/references"
|
|
22
23
|
|
|
23
24
|
module Andromeda
|
|
24
25
|
# The native extension is built per Ruby minor version, so prefer the
|
|
@@ -70,7 +70,11 @@ module Andromeda
|
|
|
70
70
|
return
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
# Without this, a file whose last line has no newline gets the first
|
|
74
|
+
# ignore line glued onto it, breaking both entries.
|
|
75
|
+
content = File.read(path)
|
|
76
|
+
separator = content.empty? || content.end_with?("\n") ? "" : "\n"
|
|
77
|
+
append_to_file filename, "#{separator}#{missing.join("\n")}\n"
|
|
74
78
|
end
|
|
75
79
|
end
|
|
76
80
|
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LIMHAUS Inc.
|
|
@@ -105,6 +105,7 @@ files:
|
|
|
105
105
|
- lib/andromeda/parser.rb
|
|
106
106
|
- lib/andromeda/pipeline.rb
|
|
107
107
|
- lib/andromeda/railtie.rb
|
|
108
|
+
- lib/andromeda/references.rb
|
|
108
109
|
- lib/andromeda/registry.rb
|
|
109
110
|
- lib/andromeda/relation.rb
|
|
110
111
|
- lib/andromeda/renderer.rb
|