antoinette 0.1.1 → 0.1.2

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: 795388e48b4799e7b321bc12ce0050797fee44d3bc6008f289c7367efe839dbd
4
- data.tar.gz: 788964274ce4ed2c0061017aee248dd58e93864d86ce56eefe65b9d13f2a115a
3
+ metadata.gz: 993d24ce2af00ab04f836fefdecc77510733c8f4ca905b396f63eaa4bb578bbc
4
+ data.tar.gz: ff2cb5009b0a07f05afb5055ff3a7547368effe0a0577913d6394bfbe16f9303
5
5
  SHA512:
6
- metadata.gz: 329ab030c6192c1af402741a0050c67059a353e8a16c3f445a99343b600666d6c1c9062d7a78ccddbf0fbc8f751c63c835d4953d9b855dd3da84b7b6458217b2
7
- data.tar.gz: 778cd2d85170e35ef3ff5a6ffe987eef2d6c728a2072466336e13307847b1ba9eec6d0b2a89fa19dc44227529d065482a9801deeb7b6b4235abb3905c7b00d8a
6
+ metadata.gz: 611a4278897c3ee934884453547a96dfeec11855732f5596dcb71c7028fd70e32caf588af64c6746a6f5f89e08a5b69f8c04a63ccfdd5f14a1310b853b3bb861
7
+ data.tar.gz: 18bb190a09f1ef55effc1c8b79597e89fb50bd84c9e8d81cc66a7d9cc170c8b80ab57069ebd3bee868fe44bfd3f5dea3bb2030d37ae2e245d59249507888b8a5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026
4
+
5
+ - Layout-aware bundling for SitePress YAML frontmatter
6
+ - Remove SHA1 digests from template marker comments
7
+ - Optional `--width` and `--height` flags for BundleGraph
8
+
3
9
  ## [0.1.0] - 2024
4
10
 
5
11
  - Initial extraction from Gridular Mod
data/README.md CHANGED
@@ -18,7 +18,19 @@ This is the problem Antoinette solves.
18
18
  Antoinette is a lightweight JS bundler which weaves Elm apps into JavaScript
19
19
  bundles, and weaves JavaScript bundles into Rails templates.
20
20
 
21
- The name comes from mansion weave, a style of flooring based on woven elm wood,
21
+ It looks at `app/views` to make a hash which maps every Elm app referenced in
22
+ the templates to every template which references them. It uses that mapping to
23
+ create a set of JS bundles such that every Rails template will download all of
24
+ the Elm apps it needs, but no Rails template will download any Elm app it
25
+ doesn't need. Antoinette also creates a visualizer, as an Elm app in a Rails
26
+ view, which you can use to understand its output.
27
+
28
+ Here's an example screenshot. In the actual Elm app, you can hover over any
29
+ given bundle to see which Elm apps it places within which Rails templates.
30
+
31
+ ![Example of Antoinette's admin visualizer](images/example.png)
32
+
33
+ The name was inspired by mansion weave, a style of flooring based on woven elm,
22
34
  which was popular in French mansions from the 16th century onwards.
23
35
 
24
36
  ## Installation
@@ -49,7 +61,8 @@ It also adds a route for `/antoinette` admin page, and adds
49
61
 
50
62
  ### Configuration
51
63
 
52
- Generate bundle configuration by analyzing which Elm apps are used in your Rails views:
64
+ Generate bundle configuration by analyzing which Elm programs your Rails
65
+ views use:
53
66
 
54
67
  ```bash
55
68
  bin/antoinette config
@@ -94,23 +107,26 @@ flow into bundles and then into Rails templates.
94
107
 
95
108
  1. **Analysis**: Scans Rails views for `Elm.AppName.init` patterns
96
109
  2. **Grouping**: Groups templates that use the same combination of Elm apps
97
- 3. **Bundling**: Compiles each group into a single JavaScript bundle (with a haiku-styled name like `holy-waterfall-8432`)
98
- 4. **Injection**: Adds `javascript_include_tag` to templates with SHA1 digest comments for idempotent updates
110
+ 3. **Bundling**: Compiles each group into a single JavaScript bundle (with a
111
+ haiku-styled name like `holy-waterfall-8432`)
112
+ 4. **Injection**: Adds `javascript_include_tag` to templates with a marker
113
+ comment for idempotent updates
99
114
 
100
115
  ### Script Tag Format
101
116
 
102
117
  Antoinette injects script tags like:
103
118
 
104
119
  ```erb
105
- <%= javascript_include_tag "antoinette/holy-waterfall-8432" %> <!-- antoinette a1b2c3d4... -->
120
+ <%= javascript_include_tag "antoinette/holy-waterfall-8432" %> <!-- antoinette -->
106
121
  ```
107
122
 
108
- Embedding a hash in the comment ensures that tags only get updated when bundle content changes.
123
+ The `<!-- antoinette -->` comment marks the line so it can be found and replaced
124
+ on subsequent builds.
109
125
 
110
126
  ## Requirements
111
127
 
112
128
  - Rails 7.0+
113
- - Elm (customize via `elm_path` in `config/antoinette.json`)
129
+ - Elm (customizable via `elm_path` in `config/antoinette.json`)
114
130
 
115
131
  ## Configuration
116
132
 
@@ -130,6 +146,14 @@ The `config/antoinette.json` file structure:
130
146
  }
131
147
  ```
132
148
 
149
+ ## SitePress Support and Layout Awareness
150
+
151
+ `custom_view_paths` exists because Antoinette has a limited layout awareness
152
+ tied into SitePress. Antoinette looks for layouts in `app/views/layouts` and in
153
+ the YAML frontmatter in SitePress content templates. It can't currently tell
154
+ which Rails templates use which Rails layouts, but it can tell which SitePress
155
+ templates use which SitePress layouts.
156
+
133
157
  ## Rake Integration
134
158
 
135
159
  The `antoinette:build` task runs automatically before `assets:precompile`:
@@ -17,8 +17,9 @@ module Antoinette
17
17
 
18
18
  option :stdout, type: :boolean, default: false, desc: "Output to stdout instead of file"
19
19
  option :custom_views, type: :array, default: [], desc: "Additional view directories to scan"
20
+ option :layout_dirs, type: :array, default: [], desc: "Additional layout directories to scan"
20
21
 
21
- def call(stdout:, custom_views: [], **)
22
+ def call(stdout:, custom_views: [], layout_dirs: [], **)
22
23
  out = Antoinette::CLI.output
23
24
  config_path = Rails.root.join("config", "antoinette.json")
24
25
 
@@ -29,19 +30,27 @@ module Antoinette
29
30
  end
30
31
  existing_custom = existing_config["custom_view_paths"] || []
31
32
  existing_elm_path = existing_config["elm_path"] || "elm"
33
+ existing_layout_dirs = existing_config["layout_dirs"] || []
32
34
  all_custom_views = (existing_custom + custom_views).uniq
35
+ all_layout_dirs = (["app/views/layouts"] + existing_layout_dirs + layout_dirs).uniq
33
36
 
34
37
  analyzer = Antoinette::ElmAppUsageAnalyzer.new(
35
- skip: "layouts/",
38
+ layout_dirs: all_layout_dirs,
36
39
  custom_view_paths: all_custom_views
37
40
  )
41
+ layout_resolver = Antoinette::LayoutResolver.new(
42
+ layout_dirs: all_layout_dirs
43
+ )
38
44
  weaver = Antoinette::Weaver.new(
39
45
  elm_analyzer: analyzer,
46
+ layout_resolver: layout_resolver,
40
47
  custom_view_paths: all_custom_views
41
48
  )
42
49
 
43
50
  output = JSON.parse(weaver.generate_json)
44
51
  output["elm_path"] = existing_elm_path
52
+ extra_layout_dirs = all_layout_dirs - ["app/views/layouts"]
53
+ output["layout_dirs"] = extra_layout_dirs if extra_layout_dirs.any?
45
54
  json_output = JSON.pretty_generate(output)
46
55
 
47
56
  if stdout
@@ -10,9 +10,9 @@ module Antoinette
10
10
  full_path = resolve_template_path(template_path)
11
11
  content = File.read(full_path)
12
12
 
13
- return unless content.match?(/<!-- antoinette [a-f0-9]+ -->/)
13
+ return unless content.match?(InjectScriptTag::MARKER)
14
14
 
15
- updated_content = content.gsub(/^.*<!-- antoinette [a-f0-9]+ -->.*\n?/, "")
15
+ updated_content = content.gsub(/^.*#{InjectScriptTag::MARKER}.*\n?/o, "")
16
16
 
17
17
  File.write(full_path, updated_content)
18
18
  end
@@ -8,8 +8,8 @@ module Antoinette
8
8
  ElmApp = Struct.new(:name)
9
9
  class Matrix < Hash; end
10
10
 
11
- def initialize(skip: nil, custom_view_paths: [])
12
- @skip = skip
11
+ def initialize(layout_dirs: ["app/views/layouts"], custom_view_paths: [])
12
+ @layout_dirs = layout_dirs
13
13
  @custom_view_paths = custom_view_paths
14
14
  end
15
15
 
@@ -21,7 +21,7 @@ module Antoinette
21
21
  next if apps.empty?
22
22
 
23
23
  relative_path = file_path.sub("#{Rails.root}/", "")
24
- next if @skip && relative_path.include?("app/views/#{@skip}")
24
+ next if in_layout_dir?(relative_path)
25
25
 
26
26
  app_names = apps.map(&:name)
27
27
  result << ViewFile.new(relative_path, app_names)
@@ -60,14 +60,6 @@ module Antoinette
60
60
  @all_app_names ||= views.flat_map(&:elm_apps).uniq.sort
61
61
  end
62
62
 
63
- def layout_apps
64
- @layout_apps ||= Dir.glob(Rails.root.join("app", "views", "layouts", "*.html.erb"))
65
- .flat_map do |file_path|
66
- content = File.read(file_path)
67
- elm_apps(content).map(&:name)
68
- end.uniq
69
- end
70
-
71
63
  def per_file
72
64
  @per_file ||= views.sort_by { |vf| -vf.elm_apps.count }
73
65
  .each_with_object({}) do |view_file, result|
@@ -96,5 +88,11 @@ module Antoinette
96
88
  end
97
89
  end
98
90
  end
91
+
92
+ private
93
+
94
+ def in_layout_dir?(relative_path)
95
+ @layout_dirs.any? { |dir| relative_path.start_with?(dir) }
96
+ end
99
97
  end
100
98
  end
@@ -1,25 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module Antoinette
6
4
  class InjectScriptTag
7
- def initialize(
8
- assets_path: Rails.root.join("app", "assets", "javascripts", "antoinette")
9
- )
10
- @assets_path = assets_path
11
- end
5
+ MARKER = /<!-- antoinette( [a-f0-9]+)? -->/
12
6
 
13
7
  def inject(template_path:, bundle_name:)
14
8
  full_path = Rails.root.join(template_path)
15
9
  content = File.read(full_path)
16
10
 
17
- bundle_path = @assets_path.join("#{bundle_name}.js")
18
- digest = Digest::SHA1.hexdigest(File.read(bundle_path))
19
- script_tag = "<%= javascript_include_tag \"antoinette/#{bundle_name}\" %> <!-- antoinette #{digest} -->"
11
+ script_tag = "<%= javascript_include_tag \"antoinette/#{bundle_name}\" %> <!-- antoinette -->"
20
12
 
21
- updated_content = if content.match?(/<!-- antoinette [a-f0-9]+ -->/)
22
- content.gsub(/^.*<!-- antoinette [a-f0-9]+ -->.*$/, script_tag)
13
+ updated_content = if content.match?(MARKER)
14
+ content.gsub(/^.*#{MARKER}.*$/o, script_tag)
23
15
  else
24
16
  content + "\n" + script_tag
25
17
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Antoinette
6
+ class LayoutResolver
7
+ def initialize(layout_dirs: ["app/views/layouts"], default_layout: "application")
8
+ @layout_dirs = layout_dirs
9
+ @default_layout = default_layout
10
+ end
11
+
12
+ def layout_apps_map
13
+ @layout_apps_map ||= @layout_dirs.each_with_object({}) do |dir, result|
14
+ Dir.glob(Rails.root.join(dir, "*.html.erb")).each do |file_path|
15
+ content = File.read(file_path)
16
+ apps = content.scan(/Elm\.(\w+)\.init/).flatten.uniq
17
+ name = File.basename(file_path, ".html.erb")
18
+ result[name] = ((result[name] || []) + apps).uniq
19
+ end
20
+ end
21
+ end
22
+
23
+ def layout_for(template_path)
24
+ full_path = if Pathname.new(template_path).absolute?
25
+ template_path
26
+ else
27
+ Rails.root.join(template_path).to_s
28
+ end
29
+
30
+ return @default_layout unless File.exist?(full_path)
31
+
32
+ content = File.read(full_path)
33
+ return @default_layout unless content.start_with?("---")
34
+
35
+ frontmatter = content.match(/\A---\s*\n(.*?\n)---/m)
36
+ return @default_layout unless frontmatter
37
+
38
+ yaml = YAML.safe_load(frontmatter[1])
39
+ yaml&.fetch("layout", @default_layout) || @default_layout
40
+ end
41
+
42
+ def apps_for(template_path)
43
+ layout = layout_for(template_path)
44
+ layout_apps_map[layout] || []
45
+ end
46
+ end
47
+ end
@@ -10,24 +10,27 @@ module Antoinette
10
10
  def initialize(
11
11
  elm_analyzer: ElmAppUsageAnalyzer.new,
12
12
  partial_resolver: PartialResolver.new,
13
+ layout_resolver: LayoutResolver.new,
13
14
  custom_view_paths: []
14
15
  )
15
16
  @elm_analyzer = elm_analyzer
16
17
  @partial_resolver = partial_resolver
18
+ @layout_resolver = layout_resolver
17
19
  @custom_view_paths = custom_view_paths
18
20
  end
19
21
 
20
22
  def bundles
21
- @bundles ||= @elm_analyzer.layout_apps.then do |layout_apps|
22
- @elm_analyzer.mappings.map do |apps, templates|
23
- resolved_templates = resolve_templates_from_partials(templates)
24
- merged_apps = (apps + layout_apps).uniq.sort
25
- Bundle.new(
26
- Haikunator.haikunate,
27
- merged_apps,
28
- resolved_templates.sort
29
- )
30
- end.sort_by { |bundle| -bundle.elm_apps.count }
23
+ @bundles ||= begin
24
+ result = []
25
+ @elm_analyzer.mappings.each do |page_apps, templates|
26
+ resolved = resolve_templates_from_partials(templates)
27
+ groups = resolved.group_by { |t| @layout_resolver.apps_for(t) }
28
+ groups.each do |layout_apps, group_templates|
29
+ merged = (page_apps + layout_apps).uniq.sort
30
+ result << Bundle.new(Haikunator.haikunate, merged, group_templates.sort)
31
+ end
32
+ end
33
+ result.sort_by { |b| -b.elm_apps.count }
31
34
  end
32
35
  end
33
36
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antoinette
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/antoinette.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "antoinette/version"
4
4
  require_relative "antoinette/services/partial_resolver"
5
+ require_relative "antoinette/services/layout_resolver"
5
6
  require_relative "antoinette/services/elm_app_usage_analyzer"
6
7
  require_relative "antoinette/services/weaver"
7
8
  require_relative "antoinette/services/compile_elm"
@@ -13,6 +13,8 @@ type alias Model =
13
13
  { bundles : List Bundle
14
14
  , selectedNodeId : Maybe String
15
15
  , lockedNodeIds : List String
16
+ , width : Maybe Int
17
+ , height : Maybe Int
16
18
  }
17
19
 
18
20
 
@@ -25,6 +27,8 @@ type alias Bundle =
25
27
 
26
28
  type alias Flags =
27
29
  { bundles : List Bundle
30
+ , width : Maybe Int
31
+ , height : Maybe Int
28
32
  }
29
33
 
30
34
 
@@ -59,6 +63,8 @@ init flags =
59
63
  ( { bundles = decoded.bundles
60
64
  , selectedNodeId = Nothing
61
65
  , lockedNodeIds = []
66
+ , width = decoded.width
67
+ , height = decoded.height
62
68
  }
63
69
  , Cmd.none
64
70
  )
@@ -67,6 +73,8 @@ init flags =
67
73
  ( { bundles = []
68
74
  , selectedNodeId = Nothing
69
75
  , lockedNodeIds = []
76
+ , width = Nothing
77
+ , height = Nothing
70
78
  }
71
79
  , Cmd.none
72
80
  )
@@ -125,8 +133,8 @@ view model =
125
133
  model.lockedNodeIds
126
134
 
127
135
  sankeyModel =
128
- { svgWidth = 1100
129
- , svgHeight = totalHeight model.bundles
136
+ { svgWidth = toFloat (Maybe.withDefault 1100 model.width)
137
+ , svgHeight = toFloat (Maybe.withDefault (totalHeightInt model.bundles) model.height)
130
138
  , columnWidths = columnWidths
131
139
  , nodePadding = 8
132
140
  , columnSpacing = 350
@@ -162,7 +170,7 @@ view model =
162
170
  , style "border" "1px solid #d1d5db"
163
171
  ]
164
172
  in
165
- div []
173
+ div [ Html.Attributes.class "bundle-graph" ]
166
174
  [ div [ style "display" "flex", style "align-items" "center", style "gap" "16px" ]
167
175
  [ h1
168
176
  [ style "font-family" "system-ui, sans-serif"
@@ -187,8 +195,8 @@ view model =
187
195
  ]
188
196
 
189
197
 
190
- totalHeight : List Bundle -> Float
191
- totalHeight bundles =
198
+ totalHeightInt : List Bundle -> Int
199
+ totalHeightInt bundles =
192
200
  let
193
201
  elmApps =
194
202
  uniqueElmApps bundles
@@ -202,7 +210,7 @@ totalHeight bundles =
202
210
  rowHeight =
203
211
  27
204
212
  in
205
- toFloat (maxItems * rowHeight + 100)
213
+ maxItems * rowHeight + 100
206
214
 
207
215
 
208
216
  buildColumnWidths : List LabelNode -> Dict.Dict Int Float
@@ -332,8 +340,10 @@ unique list =
332
340
 
333
341
  flagsDecoder : Decoder Flags
334
342
  flagsDecoder =
335
- Decode.map Flags
343
+ Decode.map3 Flags
336
344
  (Decode.field "bundles" (Decode.list bundleDecoder))
345
+ (Decode.maybe (Decode.field "width" Decode.int))
346
+ (Decode.maybe (Decode.field "height" Decode.int))
337
347
 
338
348
 
339
349
  bundleDecoder : Decoder Bundle
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antoinette
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giles Bowkett
@@ -99,6 +99,7 @@ files:
99
99
  - lib/antoinette/services/concat_bundle.rb
100
100
  - lib/antoinette/services/elm_app_usage_analyzer.rb
101
101
  - lib/antoinette/services/inject_script_tag.rb
102
+ - lib/antoinette/services/layout_resolver.rb
102
103
  - lib/antoinette/services/partial_resolver.rb
103
104
  - lib/antoinette/services/weaver.rb
104
105
  - lib/antoinette/version.rb