antoinette 0.1.0 → 0.1.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.
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "haikunator"
4
+ require "json"
5
+
6
+ module Antoinette
7
+ class Weaver
8
+ Bundle = Struct.new(:name, :elm_apps, :templates)
9
+
10
+ def initialize(
11
+ elm_analyzer: ElmAppUsageAnalyzer.new,
12
+ partial_resolver: PartialResolver.new,
13
+ custom_view_paths: []
14
+ )
15
+ @elm_analyzer = elm_analyzer
16
+ @partial_resolver = partial_resolver
17
+ @custom_view_paths = custom_view_paths
18
+ end
19
+
20
+ 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 }
31
+ end
32
+ end
33
+
34
+ def generate_json
35
+ output = {bundles: bundles.map do |bundle|
36
+ {
37
+ name: bundle.name,
38
+ elm_apps: bundle.elm_apps,
39
+ templates: bundle.templates
40
+ }
41
+ end}
42
+ output[:custom_view_paths] = @custom_view_paths if @custom_view_paths.any?
43
+ JSON.pretty_generate(output)
44
+ end
45
+
46
+ def resolve_templates_from_partials(templates)
47
+ templates.flat_map do |template|
48
+ if template.start_with?("_") || template.include?("/_")
49
+ partial_path = template.sub(%r{^app/views/}, "")
50
+ @partial_resolver.resolve(partial_path).map do |parent|
51
+ "app/views/#{parent}"
52
+ end
53
+ else
54
+ template
55
+ end
56
+ end.uniq
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Antoinette
2
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
3
5
  end
data/lib/antoinette.rb CHANGED
@@ -1,6 +1,15 @@
1
- require "antoinette/version"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "antoinette/version"
4
+ require_relative "antoinette/services/partial_resolver"
5
+ require_relative "antoinette/services/elm_app_usage_analyzer"
6
+ require_relative "antoinette/services/weaver"
7
+ require_relative "antoinette/services/compile_elm"
8
+ require_relative "antoinette/services/concat_bundle"
9
+ require_relative "antoinette/services/inject_script_tag"
10
+ require_relative "antoinette/services/clear_script_tag"
11
+ require_relative "antoinette/cli/commands"
12
+ require_relative "antoinette/engine" if defined?(Rails::Engine)
2
13
 
3
14
  module Antoinette
4
- class Error < StandardError; end
5
- # Your code goes here...
6
15
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module Antoinette
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Install Antoinette into your Rails application"
12
+
13
+ def create_config_file
14
+ create_file "config/antoinette.json", <<~JSON
15
+ {
16
+ "elm_path": "elm",
17
+ "bundles": []
18
+ }
19
+ JSON
20
+ end
21
+
22
+ def create_client_directory
23
+ empty_directory "app/client"
24
+ end
25
+
26
+ def copy_elm_files
27
+ copy_file "BundleGraph.elm", "app/client/BundleGraph.elm"
28
+ copy_file "Sankey.elm", "app/client/Sankey.elm"
29
+ end
30
+
31
+ def install_elm_dependencies
32
+ say "Installing Elm dependencies..."
33
+ run "elm install elm/browser"
34
+ run "elm install elm/html"
35
+ run "elm install elm/json"
36
+ run "elm install elm/svg"
37
+ end
38
+
39
+ def copy_controller
40
+ copy_file "graph_controller.rb", "app/controllers/antoinette/graph_controller.rb"
41
+ end
42
+
43
+ def copy_view
44
+ copy_file "show.html.erb", "app/views/antoinette/graph/show.html.erb"
45
+ end
46
+
47
+ def create_antoinette_binstub
48
+ create_file "bin/antoinette", <<~RUBY
49
+ #!/usr/bin/env ruby
50
+ require_relative "../config/environment"
51
+ Dry::CLI.new(Antoinette::CLI::Commands).call
52
+ RUBY
53
+ chmod "bin/antoinette", 0o755
54
+ end
55
+
56
+ def create_assets_directory
57
+ empty_directory "app/assets/javascripts/antoinette"
58
+ end
59
+
60
+ def update_gitignore
61
+ gitignore_path = Rails.root.join(".gitignore")
62
+ ignore_line = "app/assets/javascripts/antoinette/"
63
+
64
+ if File.exist?(gitignore_path) && !File.read(gitignore_path).include?(ignore_line)
65
+ append_to_file ".gitignore", "#{ignore_line}\n"
66
+ end
67
+ end
68
+
69
+ def add_routes
70
+ route_content = <<~RUBY
71
+ get "/antoinette", to: "antoinette/graph#show"
72
+ RUBY
73
+
74
+ route route_content
75
+ end
76
+
77
+ def show_post_install_message
78
+ say ""
79
+ say "Antoinette has been installed!", :green
80
+ say ""
81
+ say "Next steps:"
82
+ say " 1. Run `bin/antoinette config` to generate bundle configuration"
83
+ say " 2. Run `bin/antoinette build` to compile Elm bundles"
84
+ say ""
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,344 @@
1
+ module BundleGraph exposing (main)
2
+
3
+ import Browser
4
+ import Dict
5
+ import Html exposing (Html, button, div, h1, text)
6
+ import Html.Attributes exposing (disabled, style)
7
+ import Html.Events exposing (onClick)
8
+ import Json.Decode as Decode exposing (Decoder)
9
+ import Sankey exposing (layout, render)
10
+
11
+
12
+ type alias Model =
13
+ { bundles : List Bundle
14
+ , selectedNodeId : Maybe String
15
+ , lockedNodeIds : List String
16
+ }
17
+
18
+
19
+ type alias Bundle =
20
+ { name : String
21
+ , elmApps : List String
22
+ , templates : List String
23
+ }
24
+
25
+
26
+ type alias Flags =
27
+ { bundles : List Bundle
28
+ }
29
+
30
+
31
+ type alias LabelNode =
32
+ { id : String
33
+ , label : String
34
+ , column : Int
35
+ }
36
+
37
+
38
+ type Msg
39
+ = SelectNode String
40
+ | DeselectNode
41
+ | ToggleLockNode String
42
+ | ClearSelection
43
+
44
+
45
+ main : Program Decode.Value Model Msg
46
+ main =
47
+ Browser.element
48
+ { init = init
49
+ , update = update
50
+ , view = view
51
+ , subscriptions = \_ -> Sub.none
52
+ }
53
+
54
+
55
+ init : Decode.Value -> ( Model, Cmd Msg )
56
+ init flags =
57
+ case Decode.decodeValue flagsDecoder flags of
58
+ Ok decoded ->
59
+ ( { bundles = decoded.bundles
60
+ , selectedNodeId = Nothing
61
+ , lockedNodeIds = []
62
+ }
63
+ , Cmd.none
64
+ )
65
+
66
+ Err _ ->
67
+ ( { bundles = []
68
+ , selectedNodeId = Nothing
69
+ , lockedNodeIds = []
70
+ }
71
+ , Cmd.none
72
+ )
73
+
74
+
75
+ update : Msg -> Model -> ( Model, Cmd Msg )
76
+ update msg model =
77
+ case msg of
78
+ SelectNode appId ->
79
+ ( { model | selectedNodeId = Just appId }, Cmd.none )
80
+
81
+ DeselectNode ->
82
+ case model.selectedNodeId of
83
+ Just nodeId ->
84
+ if List.member nodeId model.lockedNodeIds then
85
+ ( model, Cmd.none )
86
+
87
+ else
88
+ ( { model | selectedNodeId = Nothing }, Cmd.none )
89
+
90
+ Nothing ->
91
+ ( model, Cmd.none )
92
+
93
+ ToggleLockNode nodeId ->
94
+ if List.member nodeId model.lockedNodeIds then
95
+ ( { model
96
+ | lockedNodeIds = List.filter (\id -> id /= nodeId) model.lockedNodeIds
97
+ }
98
+ , Cmd.none
99
+ )
100
+
101
+ else
102
+ ( { model | lockedNodeIds = nodeId :: model.lockedNodeIds }
103
+ , Cmd.none
104
+ )
105
+
106
+ ClearSelection ->
107
+ ( { model | selectedNodeId = Nothing, lockedNodeIds = [] }, Cmd.none )
108
+
109
+
110
+ view : Model -> Html Msg
111
+ view model =
112
+ let
113
+ nodes =
114
+ buildNodes model.bundles
115
+
116
+ columnWidths =
117
+ buildColumnWidths nodes
118
+
119
+ highlightedNodeIds =
120
+ case model.selectedNodeId of
121
+ Just id ->
122
+ id :: model.lockedNodeIds
123
+
124
+ Nothing ->
125
+ model.lockedNodeIds
126
+
127
+ sankeyModel =
128
+ { svgWidth = 1100
129
+ , svgHeight = totalHeight model.bundles
130
+ , columnWidths = columnWidths
131
+ , nodePadding = 8
132
+ , columnSpacing = 350
133
+ , edgeColor = "#60A5FA"
134
+ , edgeOpacity = 0.3
135
+ , fontSize = 11
136
+ , highlightedNodeIds = highlightedNodeIds
137
+ , lockedNodeIds = model.lockedNodeIds
138
+ }
139
+
140
+ events =
141
+ { onSelectNode = SelectNode
142
+ , onDeselectNode = DeselectNode
143
+ , onLockNode = ToggleLockNode
144
+ }
145
+
146
+ diagram =
147
+ layout sankeyModel nodes (buildEdges model.bundles)
148
+
149
+ hasSelection =
150
+ not (List.isEmpty model.lockedNodeIds)
151
+
152
+ buttonStyles =
153
+ if hasSelection then
154
+ [ style "color" "#6b7280"
155
+ , style "cursor" "pointer"
156
+ , style "border" "1px solid #374151"
157
+ ]
158
+
159
+ else
160
+ [ style "color" "#9ca3af"
161
+ , style "cursor" "not-allowed"
162
+ , style "border" "1px solid #d1d5db"
163
+ ]
164
+ in
165
+ div []
166
+ [ div [ style "display" "flex", style "align-items" "center", style "gap" "16px" ]
167
+ [ h1
168
+ [ style "font-family" "system-ui, sans-serif"
169
+ , style "margin-bottom" "10px"
170
+ , style "color" "#374151"
171
+ ]
172
+ [ text "Antoinette Bundle Graph" ]
173
+ , button
174
+ ([ onClick ClearSelection
175
+ , style "font-size" "12px"
176
+ , style "margin-bottom" "8px"
177
+ , style "border-radius" "4px"
178
+ , style "padding" "4px 8px"
179
+ , style "background" "white"
180
+ , disabled (not hasSelection)
181
+ ]
182
+ ++ buttonStyles
183
+ )
184
+ [ text "Clear Selection" ]
185
+ ]
186
+ , render sankeyModel events diagram
187
+ ]
188
+
189
+
190
+ totalHeight : List Bundle -> Float
191
+ totalHeight bundles =
192
+ let
193
+ elmApps =
194
+ uniqueElmApps bundles
195
+
196
+ templates =
197
+ uniqueTemplates bundles
198
+
199
+ maxItems =
200
+ max (List.length elmApps) (max (List.length bundles) (List.length templates))
201
+
202
+ rowHeight =
203
+ 27
204
+ in
205
+ toFloat (maxItems * rowHeight + 100)
206
+
207
+
208
+ buildColumnWidths : List LabelNode -> Dict.Dict Int Float
209
+ buildColumnWidths nodes =
210
+ let
211
+ padding =
212
+ 2
213
+
214
+ labelWidth node =
215
+ let
216
+ perCharacter =
217
+ case node.column of
218
+ 2 ->
219
+ 6
220
+
221
+ _ ->
222
+ 7
223
+ in
224
+ toFloat (padding + String.length node.label * perCharacter)
225
+
226
+ updateMax node dict =
227
+ let
228
+ currentMax =
229
+ Dict.get node.column dict |> Maybe.withDefault 0
230
+
231
+ nodeWidth =
232
+ labelWidth node
233
+ in
234
+ Dict.insert node.column (max currentMax nodeWidth) dict
235
+ in
236
+ List.foldl updateMax Dict.empty nodes
237
+
238
+
239
+ buildNodes : List Bundle -> List LabelNode
240
+ buildNodes bundles =
241
+ let
242
+ elmAppNodes =
243
+ uniqueElmApps bundles
244
+ |> List.map
245
+ (\app ->
246
+ { id = "app:" ++ app
247
+ , label = app
248
+ , column = 0
249
+ }
250
+ )
251
+
252
+ bundleNodes =
253
+ bundles
254
+ |> List.map
255
+ (\bundle ->
256
+ { id = "bundle:" ++ bundle.name
257
+ , label = bundle.name
258
+ , column = 1
259
+ }
260
+ )
261
+
262
+ templateNodes =
263
+ uniqueTemplates bundles
264
+ |> List.map
265
+ (\template ->
266
+ { id = "template:" ++ template
267
+ , label = template
268
+ , column = 2
269
+ }
270
+ )
271
+ in
272
+ elmAppNodes ++ bundleNodes ++ templateNodes
273
+
274
+
275
+ buildEdges : List Bundle -> List { fromId : String, toId : String }
276
+ buildEdges bundles =
277
+ let
278
+ appToBundleEdges =
279
+ bundles
280
+ |> List.concatMap
281
+ (\bundle ->
282
+ bundle.elmApps
283
+ |> List.map
284
+ (\app ->
285
+ { fromId = "app:" ++ app
286
+ , toId = "bundle:" ++ bundle.name
287
+ }
288
+ )
289
+ )
290
+
291
+ bundleToTemplateEdges =
292
+ bundles
293
+ |> List.concatMap
294
+ (\bundle ->
295
+ bundle.templates
296
+ |> List.map
297
+ (\template ->
298
+ { fromId = "bundle:" ++ bundle.name
299
+ , toId = "template:" ++ template
300
+ }
301
+ )
302
+ )
303
+ in
304
+ appToBundleEdges ++ bundleToTemplateEdges
305
+
306
+
307
+ uniqueElmApps : List Bundle -> List String
308
+ uniqueElmApps bundles =
309
+ bundles
310
+ |> List.concatMap .elmApps
311
+ |> List.sort
312
+ |> unique
313
+
314
+
315
+ uniqueTemplates : List Bundle -> List String
316
+ uniqueTemplates bundles =
317
+ bundles
318
+ |> List.concatMap .templates
319
+ |> List.sort
320
+ |> unique
321
+
322
+
323
+ unique : List comparable -> List comparable
324
+ unique list =
325
+ case list of
326
+ [] ->
327
+ []
328
+
329
+ first :: rest ->
330
+ first :: unique (List.filter (\x -> x /= first) rest)
331
+
332
+
333
+ flagsDecoder : Decoder Flags
334
+ flagsDecoder =
335
+ Decode.map Flags
336
+ (Decode.field "bundles" (Decode.list bundleDecoder))
337
+
338
+
339
+ bundleDecoder : Decoder Bundle
340
+ bundleDecoder =
341
+ Decode.map3 Bundle
342
+ (Decode.field "name" Decode.string)
343
+ (Decode.field "elm_apps" (Decode.list Decode.string))
344
+ (Decode.field "templates" (Decode.list Decode.string))