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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +1 -1
- data/README.md +118 -17
- data/lib/antoinette/cli/commands.rb +175 -0
- data/lib/antoinette/engine.rb +13 -0
- data/lib/antoinette/services/clear_script_tag.rb +30 -0
- data/lib/antoinette/services/compile_elm.rb +53 -0
- data/lib/antoinette/services/concat_bundle.rb +16 -0
- data/lib/antoinette/services/elm_app_usage_analyzer.rb +100 -0
- data/lib/antoinette/services/inject_script_tag.rb +30 -0
- data/lib/antoinette/services/partial_resolver.rb +68 -0
- data/lib/antoinette/services/weaver.rb +59 -0
- data/lib/antoinette/version.rb +3 -1
- data/lib/antoinette.rb +12 -3
- data/lib/generators/antoinette/install_generator.rb +88 -0
- data/lib/generators/antoinette/templates/BundleGraph.elm +344 -0
- data/lib/generators/antoinette/templates/Sankey.elm +364 -0
- data/lib/generators/antoinette/templates/graph_controller.rb +9 -0
- data/lib/generators/antoinette/templates/show.html.erb +11 -0
- data/lib/tasks/antoinette.rake +10 -0
- metadata +109 -14
- data/CODE_OF_CONDUCT.md +0 -74
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 795388e48b4799e7b321bc12ce0050797fee44d3bc6008f289c7367efe839dbd
|
|
4
|
+
data.tar.gz: 788964274ce4ed2c0061017aee248dd58e93864d86ce56eefe65b9d13f2a115a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 329ab030c6192c1af402741a0050c67059a353e8a16c3f445a99343b600666d6c1c9062d7a78ccddbf0fbc8f751c63c835d4953d9b855dd3da84b7b6458217b2
|
|
7
|
+
data.tar.gz: 778cd2d85170e35ef3ff5a6ffe987eef2d6c728a2072466336e13307847b1ba9eec6d0b2a89fa19dc44227529d065482a9801deeb7b6b4235abb3905c7b00d8a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2024
|
|
4
|
+
|
|
5
|
+
- Initial extraction from Gridular Mod
|
|
6
|
+
- Core functionality:
|
|
7
|
+
- Elm app usage analysis
|
|
8
|
+
- Bundle generation with haiku-styled names
|
|
9
|
+
- Script tag injection (idempotent)
|
|
10
|
+
- Partial template resolution
|
|
11
|
+
- CLI commands: config, build, clear, update
|
|
12
|
+
- Rails engine with admin dashboard
|
|
13
|
+
- Install generator
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1,43 +1,144 @@
|
|
|
1
1
|
# Antoinette
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Imagine you have a Rails app with numerous Elm features. You haven't gone the
|
|
6
|
+
SPA route; some Elm apps control the entire page, but many just provide bits
|
|
7
|
+
and pieces of useful functionality. This is [the officially recommended way to
|
|
8
|
+
bring Elm into a project](https://elm-lang.org/news/how-to-use-elm-at-work);
|
|
9
|
+
Evan Czaplicki called it "_the_ success path."
|
|
10
|
+
|
|
11
|
+
However, if you take that far enough, you reach a threshold where you have a
|
|
12
|
+
bunch of little Elm features throughout your site. If you don't want to switch
|
|
13
|
+
to an SPA, but you don't also want to send code down the wire that your app
|
|
14
|
+
won't use, you're at a crossroads.
|
|
15
|
+
|
|
16
|
+
This is the problem Antoinette solves.
|
|
17
|
+
|
|
18
|
+
Antoinette is a lightweight JS bundler which weaves Elm apps into JavaScript
|
|
19
|
+
bundles, and weaves JavaScript bundles into Rails templates.
|
|
20
|
+
|
|
21
|
+
The name comes from mansion weave, a style of flooring based on woven elm wood,
|
|
22
|
+
which was popular in French mansions from the 16th century onwards.
|
|
6
23
|
|
|
7
24
|
## Installation
|
|
8
25
|
|
|
9
26
|
Add this line to your application's Gemfile:
|
|
10
27
|
|
|
11
28
|
```ruby
|
|
12
|
-
gem
|
|
29
|
+
gem "antoinette"
|
|
13
30
|
```
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
Run the installer:
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
```bash
|
|
35
|
+
bin/rails generate antoinette:install
|
|
36
|
+
```
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
This creates:
|
|
39
|
+
- `config/antoinette.json` - Bundle configuration
|
|
40
|
+
- `app/client/` - Directory for Elm source files
|
|
41
|
+
- `app/client/BundleGraph.elm` and `app/client/Sankey.elm` - Admin visualization
|
|
42
|
+
- `bin/antoinette` - CLI binstub
|
|
43
|
+
- `app/assets/javascripts/antoinette/` - Bundle output directory
|
|
20
44
|
|
|
21
|
-
|
|
45
|
+
It also adds a route for `/antoinette` admin page, and adds
|
|
46
|
+
`app/assets/javascripts/antoinette` to your `.gitignore`.
|
|
22
47
|
|
|
23
48
|
## Usage
|
|
24
49
|
|
|
25
|
-
|
|
50
|
+
### Configuration
|
|
26
51
|
|
|
27
|
-
|
|
52
|
+
Generate bundle configuration by analyzing which Elm apps are used in your Rails views:
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
```bash
|
|
55
|
+
bin/antoinette config
|
|
56
|
+
```
|
|
30
57
|
|
|
31
|
-
To
|
|
58
|
+
To include custom view directories (outside `app/views/`):
|
|
32
59
|
|
|
33
|
-
|
|
60
|
+
```bash
|
|
61
|
+
bin/antoinette config --custom_views app/content/layouts/
|
|
62
|
+
```
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
### Building
|
|
36
65
|
|
|
37
|
-
|
|
66
|
+
Compile all Elm bundles and inject script tags into templates:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bin/antoinette build
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Updating Specific Apps
|
|
73
|
+
|
|
74
|
+
Rebuild only the bundles for specific Elm apps:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bin/antoinette update app/client/SearchForm.elm app/client/CaseBuilder.elm
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Clearing
|
|
81
|
+
|
|
82
|
+
Remove all generated bundles and script tags:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bin/antoinette clear
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Admin Dashboard
|
|
38
89
|
|
|
39
|
-
|
|
90
|
+
Visit `/antoinette` to see an interactive Sankey diagram showing how Elm apps
|
|
91
|
+
flow into bundles and then into Rails templates.
|
|
40
92
|
|
|
41
|
-
##
|
|
93
|
+
## How It Works
|
|
94
|
+
|
|
95
|
+
1. **Analysis**: Scans Rails views for `Elm.AppName.init` patterns
|
|
96
|
+
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
|
|
99
|
+
|
|
100
|
+
### Script Tag Format
|
|
101
|
+
|
|
102
|
+
Antoinette injects script tags like:
|
|
103
|
+
|
|
104
|
+
```erb
|
|
105
|
+
<%= javascript_include_tag "antoinette/holy-waterfall-8432" %> <!-- antoinette a1b2c3d4... -->
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Embedding a hash in the comment ensures that tags only get updated when bundle content changes.
|
|
109
|
+
|
|
110
|
+
## Requirements
|
|
111
|
+
|
|
112
|
+
- Rails 7.0+
|
|
113
|
+
- Elm (customize via `elm_path` in `config/antoinette.json`)
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
The `config/antoinette.json` file structure:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"elm_path": "elm",
|
|
122
|
+
"bundles": [
|
|
123
|
+
{
|
|
124
|
+
"name": "holy-waterfall-8432",
|
|
125
|
+
"elm_apps": ["CaseBuilder", "SearchForm"],
|
|
126
|
+
"templates": ["app/views/cases/new.html.erb"]
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"custom_view_paths": ["app/content/layouts/"]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Rake Integration
|
|
134
|
+
|
|
135
|
+
The `antoinette:build` task runs automatically before `assets:precompile`:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
rake antoinette:build
|
|
139
|
+
rake assets:precompile # runs antoinette:build first
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
42
143
|
|
|
43
|
-
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Antoinette
|
|
7
|
+
module CLI
|
|
8
|
+
def self.output
|
|
9
|
+
Rails.env.test? ? StringIO.new : $stdout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Commands
|
|
13
|
+
extend Dry::CLI::Registry
|
|
14
|
+
|
|
15
|
+
class Config < Dry::CLI::Command
|
|
16
|
+
desc "Generate JSON configuration for Elm bundles"
|
|
17
|
+
|
|
18
|
+
option :stdout, type: :boolean, default: false, desc: "Output to stdout instead of file"
|
|
19
|
+
option :custom_views, type: :array, default: [], desc: "Additional view directories to scan"
|
|
20
|
+
|
|
21
|
+
def call(stdout:, custom_views: [], **)
|
|
22
|
+
out = Antoinette::CLI.output
|
|
23
|
+
config_path = Rails.root.join("config", "antoinette.json")
|
|
24
|
+
|
|
25
|
+
existing_config = if File.exist?(config_path)
|
|
26
|
+
JSON.parse(File.read(config_path))
|
|
27
|
+
else
|
|
28
|
+
{}
|
|
29
|
+
end
|
|
30
|
+
existing_custom = existing_config["custom_view_paths"] || []
|
|
31
|
+
existing_elm_path = existing_config["elm_path"] || "elm"
|
|
32
|
+
all_custom_views = (existing_custom + custom_views).uniq
|
|
33
|
+
|
|
34
|
+
analyzer = Antoinette::ElmAppUsageAnalyzer.new(
|
|
35
|
+
skip: "layouts/",
|
|
36
|
+
custom_view_paths: all_custom_views
|
|
37
|
+
)
|
|
38
|
+
weaver = Antoinette::Weaver.new(
|
|
39
|
+
elm_analyzer: analyzer,
|
|
40
|
+
custom_view_paths: all_custom_views
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
output = JSON.parse(weaver.generate_json)
|
|
44
|
+
output["elm_path"] = existing_elm_path
|
|
45
|
+
json_output = JSON.pretty_generate(output)
|
|
46
|
+
|
|
47
|
+
if stdout
|
|
48
|
+
out.puts json_output
|
|
49
|
+
else
|
|
50
|
+
File.write(config_path, json_output)
|
|
51
|
+
out.puts "Generated #{config_path}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Build < Dry::CLI::Command
|
|
57
|
+
desc "Build JavaScript bundles from config"
|
|
58
|
+
|
|
59
|
+
def call(**)
|
|
60
|
+
out = Antoinette::CLI.output
|
|
61
|
+
config_path = Rails.root.join("config", "antoinette.json")
|
|
62
|
+
config = JSON.parse(File.read(config_path))
|
|
63
|
+
|
|
64
|
+
elm_path = config["elm_path"] || "elm"
|
|
65
|
+
compiler = Antoinette::CompileElm.new(elm_path: elm_path)
|
|
66
|
+
concatenator = Antoinette::ConcatBundle.new
|
|
67
|
+
injector = Antoinette::InjectScriptTag.new
|
|
68
|
+
|
|
69
|
+
config["bundles"].each do |bundle|
|
|
70
|
+
out.puts "Building bundle: #{bundle["name"]}"
|
|
71
|
+
|
|
72
|
+
elm_js = compiler.compile(bundle["elm_apps"])
|
|
73
|
+
concatenator.concatenate(
|
|
74
|
+
bundle_name: bundle["name"],
|
|
75
|
+
elm_js: elm_js
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
bundle["templates"].each do |template_path|
|
|
79
|
+
injector.inject(template_path: template_path, bundle_name: bundle["name"])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
out.puts " Compiled Elm apps: #{bundle["elm_apps"].join(", ")}"
|
|
83
|
+
out.puts " Injected script tags into #{bundle["templates"].length} template(s)"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
out.puts "Build complete!"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Clear < Dry::CLI::Command
|
|
91
|
+
desc "Clear generated bundles and script tags"
|
|
92
|
+
|
|
93
|
+
def call(**)
|
|
94
|
+
out = Antoinette::CLI.output
|
|
95
|
+
config_path = Rails.root.join("config", "antoinette.json")
|
|
96
|
+
config = JSON.parse(File.read(config_path))
|
|
97
|
+
|
|
98
|
+
clearer = Antoinette::ClearScriptTag.new
|
|
99
|
+
|
|
100
|
+
config["bundles"].each do |bundle|
|
|
101
|
+
out.puts "Clearing bundle: #{bundle["name"]}"
|
|
102
|
+
|
|
103
|
+
bundle_file = Rails.root.join(
|
|
104
|
+
"app", "assets", "javascripts", "antoinette", "#{bundle["name"]}.js"
|
|
105
|
+
)
|
|
106
|
+
if File.exist?(bundle_file)
|
|
107
|
+
File.delete(bundle_file)
|
|
108
|
+
out.puts " Deleted bundle file: #{bundle["name"]}.js"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
bundle["templates"].each do |template_path|
|
|
112
|
+
clearer.clear(template_path: template_path)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
out.puts " Cleared script tags from #{bundle["templates"].length} template(s)"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
out.puts "Clear complete!"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class Update < Dry::CLI::Command
|
|
123
|
+
desc "Update bundle(s) and script tag(s) for specific Elm apps"
|
|
124
|
+
|
|
125
|
+
argument :elm_files, type: :array, required: true, desc: "Elm file paths"
|
|
126
|
+
|
|
127
|
+
def call(elm_files:, **)
|
|
128
|
+
out = Antoinette::CLI.output
|
|
129
|
+
elm_app_names = elm_files.map { |path| File.basename(path, ".elm") }
|
|
130
|
+
|
|
131
|
+
config_path = Rails.root.join("config", "antoinette.json")
|
|
132
|
+
config = JSON.parse(File.read(config_path))
|
|
133
|
+
|
|
134
|
+
filtered_bundles = config["bundles"].select do |bundle|
|
|
135
|
+
(bundle["elm_apps"] & elm_app_names).any?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if filtered_bundles.empty?
|
|
139
|
+
out.puts "No bundles found containing: #{elm_app_names.join(", ")}"
|
|
140
|
+
exit
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
elm_path = config["elm_path"] || "elm"
|
|
144
|
+
compiler = Antoinette::CompileElm.new(elm_path: elm_path)
|
|
145
|
+
concatenator = Antoinette::ConcatBundle.new
|
|
146
|
+
injector = Antoinette::InjectScriptTag.new
|
|
147
|
+
|
|
148
|
+
filtered_bundles.each do |bundle|
|
|
149
|
+
out.puts "Updating bundle: #{bundle["name"]}"
|
|
150
|
+
|
|
151
|
+
elm_js = compiler.compile(bundle["elm_apps"])
|
|
152
|
+
concatenator.concatenate(
|
|
153
|
+
bundle_name: bundle["name"],
|
|
154
|
+
elm_js: elm_js
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
bundle["templates"].each do |template_path|
|
|
158
|
+
injector.inject(template_path: template_path, bundle_name: bundle["name"])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
out.puts " Compiled Elm apps: #{bundle["elm_apps"].join(", ")}"
|
|
162
|
+
out.puts " Injected script tags into #{bundle["templates"].length} template(s)"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
out.puts "Update complete!"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
register "config", Config
|
|
170
|
+
register "build", Build
|
|
171
|
+
register "clear", Clear
|
|
172
|
+
register "update", Update
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antoinette
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
initializer "antoinette.assets" do |app|
|
|
6
|
+
app.config.assets.paths << root.join("app", "assets", "javascripts")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
config.generators do |g|
|
|
10
|
+
g.test_framework :rspec
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antoinette
|
|
4
|
+
class ClearScriptTag
|
|
5
|
+
def initialize(views_path: Rails.root.join("app", "views"))
|
|
6
|
+
@views_path = views_path
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def clear(template_path:)
|
|
10
|
+
full_path = resolve_template_path(template_path)
|
|
11
|
+
content = File.read(full_path)
|
|
12
|
+
|
|
13
|
+
return unless content.match?(/<!-- antoinette [a-f0-9]+ -->/)
|
|
14
|
+
|
|
15
|
+
updated_content = content.gsub(/^.*<!-- antoinette [a-f0-9]+ -->.*\n?/, "")
|
|
16
|
+
|
|
17
|
+
File.write(full_path, updated_content)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def resolve_template_path(template_path)
|
|
23
|
+
if template_path.start_with?("app/")
|
|
24
|
+
Rails.root.join(template_path)
|
|
25
|
+
else
|
|
26
|
+
@views_path.join(template_path)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uglifier"
|
|
4
|
+
|
|
5
|
+
module Antoinette
|
|
6
|
+
class CompileElm
|
|
7
|
+
ELM_PURE_FUNCS = %w[F2 F3 F4 F5 F6 F7 F8 F9 A2 A3 A4 A5 A6 A7 A8 A9].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(elm_path: "elm", environment: Rails.env.to_s)
|
|
10
|
+
@elm_path = elm_path
|
|
11
|
+
@environment = environment
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compile(elm_app_names)
|
|
15
|
+
elm_file_paths = elm_app_names.map { |name| "app/client/#{name}.elm" }
|
|
16
|
+
output_file = "tmp/elm_compiled.js"
|
|
17
|
+
optimize_flag = production? ? "--optimize" : ""
|
|
18
|
+
|
|
19
|
+
command = "#{@elm_path} make #{optimize_flag} --output=#{output_file} #{elm_file_paths.join(" ")}".squeeze(" ")
|
|
20
|
+
|
|
21
|
+
system(command)
|
|
22
|
+
|
|
23
|
+
unless Process.last_status.success?
|
|
24
|
+
raise "Elm compilation failed"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
js = File.read(output_file)
|
|
28
|
+
production? ? minify(js) : js
|
|
29
|
+
ensure
|
|
30
|
+
File.delete(output_file) if output_file && File.exist?(output_file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def production?
|
|
36
|
+
@environment == "production"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def minify(js)
|
|
40
|
+
Uglifier.compile(
|
|
41
|
+
js,
|
|
42
|
+
compress: {
|
|
43
|
+
pure_funcs: ELM_PURE_FUNCS,
|
|
44
|
+
pure_getters: true,
|
|
45
|
+
keep_fargs: false,
|
|
46
|
+
unsafe_comps: true,
|
|
47
|
+
unsafe: true
|
|
48
|
+
},
|
|
49
|
+
mangle: true
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antoinette
|
|
4
|
+
class ConcatBundle
|
|
5
|
+
def initialize(assets_path: Rails.root.join("app", "assets", "javascripts", "antoinette"))
|
|
6
|
+
@assets_path = assets_path
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def concatenate(bundle_name:, elm_js:)
|
|
10
|
+
output_path = @assets_path.join("#{bundle_name}.js")
|
|
11
|
+
File.write(output_path, elm_js)
|
|
12
|
+
|
|
13
|
+
output_path.to_s
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
|
|
5
|
+
module Antoinette
|
|
6
|
+
class ElmAppUsageAnalyzer
|
|
7
|
+
ViewFile = Struct.new(:path, :elm_apps)
|
|
8
|
+
ElmApp = Struct.new(:name)
|
|
9
|
+
class Matrix < Hash; end
|
|
10
|
+
|
|
11
|
+
def initialize(skip: nil, custom_view_paths: [])
|
|
12
|
+
@skip = skip
|
|
13
|
+
@custom_view_paths = custom_view_paths
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def views
|
|
17
|
+
@views ||= view_path_globs.each_with_object([]) do |glob_pattern, result|
|
|
18
|
+
Dir.glob(glob_pattern).each do |file_path|
|
|
19
|
+
content = File.read(file_path)
|
|
20
|
+
apps = elm_apps(content)
|
|
21
|
+
next if apps.empty?
|
|
22
|
+
|
|
23
|
+
relative_path = file_path.sub("#{Rails.root}/", "")
|
|
24
|
+
next if @skip && relative_path.include?("app/views/#{@skip}")
|
|
25
|
+
|
|
26
|
+
app_names = apps.map(&:name)
|
|
27
|
+
result << ViewFile.new(relative_path, app_names)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def view_path_globs
|
|
33
|
+
globs = [Rails.root.join("app", "views", "**", "*.html.erb")]
|
|
34
|
+
@custom_view_paths.each do |custom_path|
|
|
35
|
+
full_path = Rails.root.join(custom_path)
|
|
36
|
+
globs << if File.file?(full_path)
|
|
37
|
+
full_path
|
|
38
|
+
else
|
|
39
|
+
full_path.join("**", "*.html.erb")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
globs
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def elm_apps(content)
|
|
46
|
+
app_names = content.scan(/Elm\.(\w+)\.init/).flatten.uniq
|
|
47
|
+
app_names.map { |name| ElmApp.new(name) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def matrix
|
|
51
|
+
@matrix ||= Matrix.new.tap do |m|
|
|
52
|
+
all_app_names.each do |app_name|
|
|
53
|
+
m[app_name] = views.select { |v| v.elm_apps.include?(app_name) }
|
|
54
|
+
.map(&:path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def all_app_names
|
|
60
|
+
@all_app_names ||= views.flat_map(&:elm_apps).uniq.sort
|
|
61
|
+
end
|
|
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
|
+
def per_file
|
|
72
|
+
@per_file ||= views.sort_by { |vf| -vf.elm_apps.count }
|
|
73
|
+
.each_with_object({}) do |view_file, result|
|
|
74
|
+
result[view_file.path] = view_file.elm_apps
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def mappings
|
|
79
|
+
@mappings ||= views.group_by { |vf| vf.elm_apps.sort }
|
|
80
|
+
.sort_by { |apps, _| -apps.count }
|
|
81
|
+
.each_with_object({}) do |(apps, view_files), result|
|
|
82
|
+
result[apps] = view_files.map(&:path).sort
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def generate_csv
|
|
87
|
+
CSV.generate do |csv|
|
|
88
|
+
csv << ["View File"] + all_app_names
|
|
89
|
+
|
|
90
|
+
views.each do |view_file|
|
|
91
|
+
row = [view_file.path]
|
|
92
|
+
all_app_names.each do |app_name|
|
|
93
|
+
row << (view_file.elm_apps.include?(app_name) ? "X" : "")
|
|
94
|
+
end
|
|
95
|
+
csv << row
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Antoinette
|
|
6
|
+
class InjectScriptTag
|
|
7
|
+
def initialize(
|
|
8
|
+
assets_path: Rails.root.join("app", "assets", "javascripts", "antoinette")
|
|
9
|
+
)
|
|
10
|
+
@assets_path = assets_path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def inject(template_path:, bundle_name:)
|
|
14
|
+
full_path = Rails.root.join(template_path)
|
|
15
|
+
content = File.read(full_path)
|
|
16
|
+
|
|
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} -->"
|
|
20
|
+
|
|
21
|
+
updated_content = if content.match?(/<!-- antoinette [a-f0-9]+ -->/)
|
|
22
|
+
content.gsub(/^.*<!-- antoinette [a-f0-9]+ -->.*$/, script_tag)
|
|
23
|
+
else
|
|
24
|
+
content + "\n" + script_tag
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
File.write(full_path, updated_content)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antoinette
|
|
4
|
+
class PartialResolver
|
|
5
|
+
RenderCall = Struct.new(:template_path, :partial_path)
|
|
6
|
+
|
|
7
|
+
def renders
|
|
8
|
+
@renders ||= begin
|
|
9
|
+
view_files = Dir.glob(Rails.root.join("app", "views", "**", "*.html.erb"))
|
|
10
|
+
|
|
11
|
+
view_files.each_with_object([]) do |file_path, result|
|
|
12
|
+
content = File.read(file_path)
|
|
13
|
+
relative_template_path = file_path.sub("#{Rails.root}/app/views/", "")
|
|
14
|
+
partial_paths = extract_partial_paths(content, relative_template_path)
|
|
15
|
+
next if partial_paths.empty?
|
|
16
|
+
|
|
17
|
+
partial_paths.each do |partial_path|
|
|
18
|
+
result << RenderCall.new(relative_template_path, partial_path)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def extract_partial_paths(content, template_path = nil)
|
|
25
|
+
paths = []
|
|
26
|
+
|
|
27
|
+
content.scan(/render\s+partial:\s*["']([^"']+)["']/) do |match|
|
|
28
|
+
paths << normalize_partial_path(match[0], template_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
content.scan(/render\s+["']([^"']+)["']/) do |match|
|
|
32
|
+
paths << normalize_partial_path(match[0], template_path)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
paths.uniq
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def normalize_partial_path(path, template_path = nil)
|
|
39
|
+
if path.include?("/")
|
|
40
|
+
dir, name = path.split("/")[0..-2].join("/"), path.split("/").last
|
|
41
|
+
name = name.start_with?("_") ? name : "_#{name}"
|
|
42
|
+
"#{dir}/#{name}.html.erb"
|
|
43
|
+
else
|
|
44
|
+
name = path.start_with?("_") ? path : "_#{path}"
|
|
45
|
+
|
|
46
|
+
if template_path
|
|
47
|
+
template_dir = File.dirname(template_path)
|
|
48
|
+
"#{template_dir}/#{name}.html.erb"
|
|
49
|
+
else
|
|
50
|
+
"#{name}.html.erb"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def partials
|
|
56
|
+
@partials ||= begin
|
|
57
|
+
grouped = renders.group_by(&:partial_path)
|
|
58
|
+
grouped.transform_values do |render_calls|
|
|
59
|
+
render_calls.map(&:template_path).sort
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve(partial_path)
|
|
65
|
+
partials[partial_path] || []
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|