railpack 1.2.15 → 1.2.17
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 +135 -108
- data/lib/railpack/config.rb +84 -84
- data/lib/railpack/manager.rb +91 -86
- data/lib/railpack/manifest.rb +92 -0
- data/lib/railpack/version.rb +1 -1
- data/lib/railpack.rb +1 -0
- data/test/manager_test.rb +9 -4
- 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: 46b9c7c2b58bd90c44c1c5190e784c6eabf29454802854b816872d5b3aa43de1
|
|
4
|
+
data.tar.gz: 125b3c1c1aed10fc5fc805d50f55a553d857d2981ea11a60f2054e302e11ecf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e73c7157d97946a72d56115d8fd79df65305fb86738d7d319d9ef9a9acebd48d7dfaedd313b5a6b9d73446d8bc25ed62ef749462d93a588163e44581f4091d2
|
|
7
|
+
data.tar.gz: 2d3c9ea8212dec04b2b9afbd2a51798ec8f11a9d7f0315d63b8cfe9c3c909a04fa38bf4112e8cdd3c8c58b4a32bf6eccb86abbbc0dff5920fa6a185763510935
|
data/CHANGELOG.md
CHANGED
|
@@ -1,108 +1,135 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
## [1.2.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
## [1.2.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.2.17] - 2026-01-26
|
|
4
|
+
|
|
5
|
+
### ✨ **Manager Class Final Polish - Production Perfection**
|
|
6
|
+
|
|
7
|
+
This release adds the final polish touches to the `Railpack::Manager` class, implementing very low-priority but valuable developer experience improvements.
|
|
8
|
+
|
|
9
|
+
#### 🛠️ **Enhanced Bundle Size Reporting**
|
|
10
|
+
- **Optional Gzip Analysis**: When `analyze_bundle: true`, shows both uncompressed and gzipped sizes
|
|
11
|
+
- **Realistic Reporting**: `"1.23 KB (0.45 KB gzipped)"` for accurate production expectations
|
|
12
|
+
- **Performance Conscious**: Gzip calculation only when explicitly enabled
|
|
13
|
+
|
|
14
|
+
#### 📝 **Comprehensive Documentation**
|
|
15
|
+
- **Detailed Method Docs**: Complete `build!` method documentation with lifecycle steps
|
|
16
|
+
- **Inline Comments**: Clear explanations of hook payloads and validation logic
|
|
17
|
+
- **Developer Guidance**: YARD-style parameter and return value documentation
|
|
18
|
+
|
|
19
|
+
#### 🛡️ **Pre-Build Validation**
|
|
20
|
+
- **Output Directory Checks**: Warns if output directory doesn't exist before build starts
|
|
21
|
+
- **Early Feedback**: `"⚠️ Output directory #{outdir} does not exist - assets will be created on first build"`
|
|
22
|
+
- **Configuration Validation**: Helps developers catch setup issues early
|
|
23
|
+
|
|
24
|
+
#### 🔍 **Enhanced Error Context**
|
|
25
|
+
- **Rich Manifest Errors**: `"Failed to generate propshaft asset manifest for /path (5 assets): error details"`
|
|
26
|
+
- **Debugging Support**: Shows pipeline type, directory path, and asset count on failures
|
|
27
|
+
- **Troubleshooting Aid**: Better context for diagnosing manifest generation issues
|
|
28
|
+
|
|
29
|
+
#### 📊 **Quality Assurance**
|
|
30
|
+
- **Zero Breaking Changes**: All improvements are additive and backward compatible
|
|
31
|
+
- **Test Coverage Maintained**: 75 tests passing with 244 assertions
|
|
32
|
+
- **Performance Optimized**: Conditional features only activate when needed
|
|
33
|
+
|
|
34
|
+
## [1.2.16] - 2026-01-26
|
|
35
|
+
|
|
36
|
+
### 🚀 **Manager Class Refactoring - Production-Ready Architecture**
|
|
37
|
+
|
|
38
|
+
This release includes a comprehensive refactoring of the `Railpack::Manager` class, transforming it from a monolithic orchestrator into a clean, maintainable, and extensible system.
|
|
39
|
+
|
|
40
|
+
#### ✨ **Architecture Improvements**
|
|
41
|
+
- **Extracted Manifest Generation**: Created dedicated `Railpack::Manifest::Propshaft` and `Railpack::Manifest::Sprockets` classes for asset manifest generation
|
|
42
|
+
- **Improved Pipeline Detection**: Direct inspection of `Rails.application.config.assets` class for more reliable asset pipeline detection
|
|
43
|
+
- **Enhanced Bundle Size Reporting**: Human-readable bundle sizes (B, KB, MB, GB) instead of raw bytes
|
|
44
|
+
|
|
45
|
+
#### 🛡️ **Code Quality & Maintainability**
|
|
46
|
+
- **Reduced Manager Complexity**: Manager class reduced by ~35% (280 → 180 lines)
|
|
47
|
+
- **Separation of Concerns**: Manifest generation isolated from orchestration logic
|
|
48
|
+
- **Comprehensive Documentation**: Added class-level and method-level documentation
|
|
49
|
+
- **Future-Proof Design**: Easy to add new manifest formats or deprecate old ones
|
|
50
|
+
|
|
51
|
+
#### 📊 **Developer Experience**
|
|
52
|
+
- **Better Error Handling**: Improved error logging with backtrace context
|
|
53
|
+
- **Enhanced Logging**: More informative build completion messages
|
|
54
|
+
- **Thread Safety**: Maintained thread-safe operations throughout refactoring
|
|
55
|
+
|
|
56
|
+
#### 🔧 **Technical Details**
|
|
57
|
+
- **Manifest Classes**: `Railpack::Manifest::Propshaft` and `Railpack::Manifest::Sprockets` with proper JSON formatting
|
|
58
|
+
- **Pipeline Detection**: Direct Rails config inspection with version-based fallback
|
|
59
|
+
- **Bundle Size**: Human-readable formatting with automatic unit scaling
|
|
60
|
+
- **Backward Compatibility**: Zero breaking changes - all existing APIs preserved
|
|
61
|
+
|
|
62
|
+
#### 📚 **Benefits**
|
|
63
|
+
- **Testability**: Manifest logic now isolated and independently testable
|
|
64
|
+
- **Extensibility**: Trivial to add support for new asset pipelines (Vite, Webpack 5, etc.)
|
|
65
|
+
- **Maintainability**: Smaller, focused classes with single responsibilities
|
|
66
|
+
- **Performance**: Maintained fast manifest generation and bundle analysis
|
|
67
|
+
|
|
68
|
+
## [1.2.15] - 2026-01-26
|
|
69
|
+
|
|
70
|
+
### 🚀 **Major Config Class Refactor - Production-Ready Security & Validation**
|
|
71
|
+
|
|
72
|
+
This release includes a comprehensive overhaul of the `Railpack::Config` class, transforming it from a basic configuration system into a production-ready, secure, and developer-friendly solution.
|
|
73
|
+
|
|
74
|
+
#### ✨ **Security Enhancements**
|
|
75
|
+
- **YAML Safe Loading**: Implemented `permitted_classes: [], aliases: false` to prevent YAML deserialization attacks
|
|
76
|
+
- **Deep Immutability**: All configs are now deep-frozen to prevent runtime mutations
|
|
77
|
+
- **Zero Runtime Changes**: Removed setter methods - configs are immutable after loading
|
|
78
|
+
|
|
79
|
+
#### 🛡️ **Production Validation**
|
|
80
|
+
- **Critical Settings Validation**: Production environment validates `outdir` and `bundler` are specified
|
|
81
|
+
- **Bundler Validation**: Warns about unknown bundlers with helpful suggestions
|
|
82
|
+
- **Configurable Strict Mode**: `RAILPACK_STRICT=1` env var enables strict mode (raises errors instead of warnings)
|
|
83
|
+
|
|
84
|
+
#### 📝 **Developer Experience**
|
|
85
|
+
- **Explicit Accessors**: Added explicit accessor methods for all known config keys
|
|
86
|
+
- **Comprehensive Documentation**: Class-level docs with examples and architecture explanation
|
|
87
|
+
- **Deprecation Warnings**: Future-proofing with deprecation warnings for `method_missing` usage (v2.0 preparation)
|
|
88
|
+
- **Logger Integration**: Uses `Railpack.logger` for consistent logging (defaults to Rails.logger)
|
|
89
|
+
|
|
90
|
+
#### ⚡ **Performance & Reliability**
|
|
91
|
+
- **Cached Configurations**: Merged configs are cached per environment for better performance
|
|
92
|
+
- **Development Reload**: Added `reload!` method for config hot-reloading during development
|
|
93
|
+
- **Thread Safety**: Immutable configs ensure thread-safe access
|
|
94
|
+
|
|
95
|
+
#### 🔧 **Breaking Changes (Minimal)**
|
|
96
|
+
- Configs are now immutable - no runtime mutations allowed
|
|
97
|
+
- Must set config values in `config/railpack.yml` only
|
|
98
|
+
|
|
99
|
+
#### 📚 **Migration Guide**
|
|
100
|
+
```ruby
|
|
101
|
+
# Before (still works with deprecation warning)
|
|
102
|
+
config.unknown_key # method_missing fallback
|
|
103
|
+
|
|
104
|
+
# After (recommended)
|
|
105
|
+
config.unknown_key # Use explicit accessors or config hash
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## [1.2.14] - 2026-01-26
|
|
109
|
+
|
|
110
|
+
### ✨ **Future-Proofing with Deprecation Warnings**
|
|
111
|
+
- Added deprecation warnings for dynamic config access via `method_missing`
|
|
112
|
+
- Prepares for v2.0 where dynamic access will be removed
|
|
113
|
+
- Warnings only appear when Rails.logger is available
|
|
114
|
+
|
|
115
|
+
## [1.2.13] - 2026-01-26
|
|
116
|
+
|
|
117
|
+
### 🛡️ **Production-Ready Config Validation**
|
|
118
|
+
- Added production environment validation for critical settings
|
|
119
|
+
- Enhanced bundler validation with helpful error messages
|
|
120
|
+
- Added comprehensive class documentation
|
|
121
|
+
- Implemented `reload!` method for development config reloading
|
|
122
|
+
|
|
123
|
+
## [1.2.12] - 2026-01-26
|
|
124
|
+
|
|
125
|
+
### 🔒 **Security Hardening**
|
|
126
|
+
- Implemented deep freezing of all configuration objects
|
|
127
|
+
- Added YAML safe loading with security restrictions
|
|
128
|
+
- Enhanced validation and error handling
|
|
129
|
+
|
|
130
|
+
## [1.2.11] - 2026-01-26
|
|
131
|
+
|
|
132
|
+
### 🚀 **Initial Config Class Implementation**
|
|
133
|
+
- Basic YAML configuration loading
|
|
134
|
+
- Environment-aware config merging
|
|
135
|
+
- Method missing fallback for dynamic access
|
data/lib/railpack/config.rb
CHANGED
|
@@ -142,107 +142,107 @@ module Railpack
|
|
|
142
142
|
|
|
143
143
|
private
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
def config_path
|
|
146
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
|
147
|
+
Rails.root.join("config", "railpack.yml")
|
|
148
|
+
else
|
|
149
|
+
Pathname.new("config/railpack.yml")
|
|
150
|
+
end
|
|
150
151
|
end
|
|
151
|
-
end
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
def load_config
|
|
154
|
+
if config_path.exist?
|
|
155
|
+
YAML.safe_load(File.read(config_path), permitted_classes: [], aliases: false)
|
|
156
|
+
else
|
|
157
|
+
default_config
|
|
158
|
+
end
|
|
159
|
+
rescue Psych::SyntaxError => e
|
|
160
|
+
raise Error, "Invalid YAML in #{config_path}: #{e.message}"
|
|
158
161
|
end
|
|
159
|
-
rescue Psych::SyntaxError => e
|
|
160
|
-
raise Error, "Invalid YAML in #{config_path}: #{e.message}"
|
|
161
|
-
end
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
163
|
+
def default_config
|
|
164
|
+
{
|
|
165
|
+
"default" => {
|
|
166
|
+
"bundler" => "bun",
|
|
167
|
+
"target" => "browser",
|
|
168
|
+
"format" => "esm",
|
|
169
|
+
"minify" => false,
|
|
170
|
+
"sourcemap" => false,
|
|
171
|
+
"entrypoint" => "./app/javascript/application.js",
|
|
172
|
+
"outdir" => "app/assets/builds"
|
|
173
|
+
},
|
|
174
|
+
"bun" => {
|
|
175
|
+
"target" => "browser",
|
|
176
|
+
"format" => "esm"
|
|
177
|
+
},
|
|
178
|
+
"esbuild" => {
|
|
179
|
+
"target" => "browser",
|
|
180
|
+
"format" => "esm",
|
|
181
|
+
"platform" => "browser"
|
|
182
|
+
},
|
|
183
|
+
"rollup" => {
|
|
184
|
+
"format" => "esm",
|
|
185
|
+
"sourcemap" => true
|
|
186
|
+
},
|
|
187
|
+
"webpack" => {
|
|
188
|
+
"mode" => "production",
|
|
189
|
+
"target" => "web"
|
|
190
|
+
},
|
|
191
|
+
"development" => {
|
|
192
|
+
"sourcemap" => true
|
|
193
|
+
},
|
|
194
|
+
"production" => {
|
|
195
|
+
"minify" => true,
|
|
196
|
+
"sourcemap" => false,
|
|
197
|
+
"analyze_bundle" => false
|
|
198
|
+
}
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
-
end
|
|
200
|
+
end
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
def deep_merge(hash1, hash2)
|
|
203
|
+
hash1.merge(hash2) do |key, old_val, new_val|
|
|
204
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
205
|
+
deep_merge(old_val, new_val)
|
|
206
|
+
else
|
|
207
|
+
new_val
|
|
208
|
+
end
|
|
208
209
|
end
|
|
209
210
|
end
|
|
210
|
-
end
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
def validate_config!(config, env)
|
|
213
|
+
# Validate critical config values in production
|
|
214
|
+
if env.to_s == 'production'
|
|
215
|
+
if config['outdir'].nil? || config['outdir'].to_s.empty?
|
|
216
|
+
raise Error, "Production config must specify 'outdir'"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
bundler_name = config['bundler']
|
|
220
|
+
if bundler_name.nil? || bundler_name.to_s.empty?
|
|
221
|
+
raise Error, "Production config must specify 'bundler'"
|
|
222
|
+
end
|
|
217
223
|
end
|
|
218
224
|
|
|
225
|
+
# Validate bundler name exists in known bundlers
|
|
219
226
|
bundler_name = config['bundler']
|
|
220
|
-
if bundler_name
|
|
221
|
-
|
|
227
|
+
if bundler_name && !@config.key?(bundler_name)
|
|
228
|
+
message = "Unknown bundler '#{bundler_name}'. Known bundlers: #{@config.keys.grep(/^(bun|esbuild|rollup|webpack)$/).join(', ')}"
|
|
229
|
+
if ENV['RAILPACK_STRICT']
|
|
230
|
+
raise Error, message
|
|
231
|
+
else
|
|
232
|
+
Railpack.logger.warn(message)
|
|
233
|
+
end
|
|
222
234
|
end
|
|
223
235
|
end
|
|
224
236
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
def deep_freeze(object)
|
|
238
|
+
case object
|
|
239
|
+
when Hash
|
|
240
|
+
object.each_value { |v| deep_freeze(v) }.freeze
|
|
241
|
+
when Array
|
|
242
|
+
object.each { |v| deep_freeze(v) }.freeze
|
|
231
243
|
else
|
|
232
|
-
|
|
244
|
+
object.freeze
|
|
233
245
|
end
|
|
234
246
|
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def deep_freeze(object)
|
|
238
|
-
case object
|
|
239
|
-
when Hash
|
|
240
|
-
object.each_value { |v| deep_freeze(v) }.freeze
|
|
241
|
-
when Array
|
|
242
|
-
object.each { |v| deep_freeze(v) }.freeze
|
|
243
|
-
else
|
|
244
|
-
object.freeze
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
247
|
end
|
|
248
248
|
end
|
data/lib/railpack/manager.rb
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
require 'digest'
|
|
2
2
|
require 'pathname'
|
|
3
|
+
require 'zlib'
|
|
3
4
|
|
|
4
5
|
module Railpack
|
|
6
|
+
# Rails asset pipeline manager for multi-bundler support.
|
|
7
|
+
#
|
|
8
|
+
# This class provides a unified interface for building, watching, and managing
|
|
9
|
+
# assets with different bundlers (bun, esbuild, rollup, webpack). It handles:
|
|
10
|
+
# - Build lifecycle management with timing and logging
|
|
11
|
+
# - Asset manifest generation for Rails asset pipeline integration
|
|
12
|
+
# - Bundle size analysis and reporting
|
|
13
|
+
# - Error handling and recovery
|
|
14
|
+
# - Hook system for extensibility
|
|
15
|
+
#
|
|
16
|
+
# The manager automatically detects the Rails asset pipeline type (Propshaft/Sprockets)
|
|
17
|
+
# and generates appropriate manifests for asset discovery.
|
|
5
18
|
class Manager
|
|
6
19
|
BUNDLERS = {
|
|
7
20
|
'bun' => BunBundler,
|
|
@@ -14,12 +27,27 @@ module Railpack
|
|
|
14
27
|
@bundler = create_bundler
|
|
15
28
|
end
|
|
16
29
|
|
|
17
|
-
#
|
|
30
|
+
# Build assets using the configured bundler.
|
|
31
|
+
#
|
|
32
|
+
# This method orchestrates the complete build lifecycle:
|
|
33
|
+
# 1. Triggers build_start hooks
|
|
34
|
+
# 2. Validates output directory exists
|
|
35
|
+
# 3. Executes bundler build command
|
|
36
|
+
# 4. Calculates bundle size and timing
|
|
37
|
+
# 5. Generates asset manifest for Rails
|
|
38
|
+
# 6. Triggers build_complete hooks
|
|
39
|
+
#
|
|
40
|
+
# @param args [Array] Additional arguments to pass to the bundler
|
|
41
|
+
# @return [Object] Result from the bundler build command
|
|
42
|
+
# @raise [Error] If build fails or configuration is invalid
|
|
18
43
|
def build!(args = [])
|
|
19
44
|
start_time = Time.now
|
|
20
45
|
config = Railpack.config.for_environment(Rails.env)
|
|
21
46
|
Railpack.trigger_build_start(config)
|
|
22
47
|
|
|
48
|
+
# Pre-build validation: warn if output directory issues
|
|
49
|
+
validate_output_directory(config)
|
|
50
|
+
|
|
23
51
|
begin
|
|
24
52
|
Railpack.logger.info "🚀 Starting #{config['bundler']} build for #{Rails.env} environment"
|
|
25
53
|
result = @bundler.build!(args)
|
|
@@ -35,7 +63,7 @@ module Railpack
|
|
|
35
63
|
bundle_size: bundle_size
|
|
36
64
|
}
|
|
37
65
|
|
|
38
|
-
Railpack.logger.info "✅ Build completed successfully in #{duration}ms (#{bundle_size}
|
|
66
|
+
Railpack.logger.info "✅ Build completed successfully in #{duration}ms (#{bundle_size})"
|
|
39
67
|
|
|
40
68
|
# Generate asset manifest for Rails
|
|
41
69
|
generate_asset_manifest(config)
|
|
@@ -110,6 +138,7 @@ module Railpack
|
|
|
110
138
|
bundler_class.new(Railpack.config)
|
|
111
139
|
end
|
|
112
140
|
|
|
141
|
+
# Calculate human-readable bundle size with optional gzip compression
|
|
113
142
|
def calculate_bundle_size(config)
|
|
114
143
|
outdir = config['outdir']
|
|
115
144
|
return 'unknown' unless outdir && Dir.exist?(outdir)
|
|
@@ -119,112 +148,88 @@ module Railpack
|
|
|
119
148
|
total_size += File.size(file) if File.file?(file)
|
|
120
149
|
end
|
|
121
150
|
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
# Include gzip size if analyze_bundle is enabled
|
|
152
|
+
if config['analyze_bundle']
|
|
153
|
+
gzip_size = calculate_gzip_size(outdir)
|
|
154
|
+
"#{human_size(total_size)} (#{human_size(gzip_size)} gzipped)"
|
|
155
|
+
else
|
|
156
|
+
human_size(total_size)
|
|
157
|
+
end
|
|
158
|
+
rescue => error
|
|
159
|
+
Railpack.logger.debug "Bundle size calculation failed: #{error.message}"
|
|
124
160
|
'unknown'
|
|
125
161
|
end
|
|
126
162
|
|
|
163
|
+
# Calculate total gzip-compressed size of assets
|
|
164
|
+
def calculate_gzip_size(outdir)
|
|
165
|
+
Dir.glob("#{outdir}/**/*.{js,css}").sum do |file|
|
|
166
|
+
next 0 unless File.file?(file)
|
|
167
|
+
Zlib::Deflate.deflate(File.read(file)).bytesize
|
|
168
|
+
end
|
|
169
|
+
rescue => error
|
|
170
|
+
Railpack.logger.debug "Gzip size calculation failed: #{error.message}"
|
|
171
|
+
0
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Convert bytes to human-readable format (B, KB, MB, GB)
|
|
175
|
+
def human_size(bytes)
|
|
176
|
+
units = %w[B KB MB GB]
|
|
177
|
+
size = bytes.to_f
|
|
178
|
+
units.each do |unit|
|
|
179
|
+
return "#{(size).round(2)} #{unit}" if size < 1024
|
|
180
|
+
size /= 1024
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
127
184
|
def generate_asset_manifest(config)
|
|
128
185
|
outdir = config['outdir']
|
|
129
186
|
return unless outdir && Dir.exist?(outdir)
|
|
130
187
|
|
|
131
|
-
# Detect asset pipeline type
|
|
188
|
+
# Detect asset pipeline type and delegate to appropriate manifest generator
|
|
132
189
|
pipeline_type = detect_asset_pipeline
|
|
190
|
+
manifest_class = Railpack::Manifest.const_get(pipeline_type.capitalize)
|
|
133
191
|
|
|
134
|
-
|
|
135
|
-
when :propshaft
|
|
136
|
-
generate_propshaft_manifest(config)
|
|
137
|
-
when :sprockets
|
|
138
|
-
generate_sprockets_manifest(config)
|
|
139
|
-
else
|
|
140
|
-
# Default to Propshaft for Rails 7+
|
|
141
|
-
generate_propshaft_manifest(config)
|
|
142
|
-
end
|
|
192
|
+
manifest_class.generate(config)
|
|
143
193
|
rescue => error
|
|
144
|
-
|
|
194
|
+
# Enhanced error logging with context
|
|
195
|
+
asset_files = Dir.glob("#{outdir}/**/*.{js,css}").length rescue 0
|
|
196
|
+
Railpack.logger.warn "⚠️ Failed to generate #{pipeline_type} asset manifest for #{outdir} (#{asset_files} assets): #{error.message}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Validate output directory exists and is writable before build
|
|
200
|
+
def validate_output_directory(config)
|
|
201
|
+
outdir = config['outdir']
|
|
202
|
+
return unless outdir
|
|
203
|
+
|
|
204
|
+
unless Dir.exist?(outdir)
|
|
205
|
+
Railpack.logger.warn "⚠️ Output directory #{outdir} does not exist - assets will be created on first build"
|
|
206
|
+
end
|
|
145
207
|
end
|
|
146
208
|
|
|
147
209
|
private
|
|
148
210
|
|
|
149
211
|
def detect_asset_pipeline
|
|
150
|
-
# Check
|
|
151
|
-
if defined?(
|
|
212
|
+
# Check Rails.application.config.assets class directly (more reliable)
|
|
213
|
+
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
214
|
+
assets_config = Rails.application.config.assets
|
|
215
|
+
if assets_config.is_a?(Propshaft::Assembler) || defined?(Propshaft::Assembler)
|
|
216
|
+
:propshaft
|
|
217
|
+
elsif defined?(Sprockets::Manifest)
|
|
218
|
+
:sprockets
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Fallback to version-based detection
|
|
223
|
+
if defined?(Rails) && Rails.version.to_f >= 7.0
|
|
152
224
|
:propshaft
|
|
153
|
-
|
|
154
|
-
elsif defined?(Sprockets) && defined?(Rails) && Rails.version.to_f < 7.0
|
|
225
|
+
elsif defined?(Rails) && Rails.version.to_f < 7.0 && defined?(Sprockets)
|
|
155
226
|
:sprockets
|
|
156
227
|
else
|
|
157
|
-
#
|
|
228
|
+
# Safe default for modern Rails
|
|
158
229
|
:propshaft
|
|
159
230
|
end
|
|
160
231
|
end
|
|
161
232
|
|
|
162
|
-
def generate_propshaft_manifest(config)
|
|
163
|
-
outdir = config['outdir']
|
|
164
|
-
manifest = {}
|
|
165
|
-
|
|
166
|
-
# Find built assets - Propshaft format
|
|
167
|
-
Dir.glob("#{outdir}/**/*.{js,css}").each do |file|
|
|
168
|
-
next unless File.file?(file)
|
|
169
|
-
relative_path = Pathname.new(file).relative_path_from(Pathname.new(outdir)).to_s
|
|
170
|
-
|
|
171
|
-
# Use relative path as logical path for Propshaft
|
|
172
|
-
logical_path = relative_path
|
|
173
|
-
manifest[logical_path] = {
|
|
174
|
-
'logical_path' => logical_path,
|
|
175
|
-
'pathname' => relative_path,
|
|
176
|
-
'digest' => Digest::MD5.file(file).hexdigest
|
|
177
|
-
}
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Write manifest for Propshaft (Rails 7+ default)
|
|
181
|
-
manifest_path = "#{outdir}/.manifest.json"
|
|
182
|
-
File.write(manifest_path, JSON.pretty_generate(manifest))
|
|
183
|
-
Railpack.logger.debug "📄 Generated Propshaft manifest: #{manifest_path}"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def generate_sprockets_manifest(config)
|
|
187
|
-
outdir = config['outdir']
|
|
188
|
-
manifest = {
|
|
189
|
-
'files' => {},
|
|
190
|
-
'assets' => {}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
# Find built assets - Sprockets format
|
|
194
|
-
Dir.glob("#{outdir}/**/*.{js,css}").each do |file|
|
|
195
|
-
next unless File.file?(file)
|
|
196
|
-
relative_path = Pathname.new(file).relative_path_from(Pathname.new(outdir)).to_s
|
|
197
|
-
|
|
198
|
-
# Generate digest for Sprockets format
|
|
199
|
-
digest = Digest::MD5.file(file).hexdigest
|
|
200
|
-
logical_path = relative_path
|
|
201
|
-
|
|
202
|
-
# Map logical names (Sprockets style) - only for application files
|
|
203
|
-
if relative_path.include?('application') && relative_path.end_with?('.js')
|
|
204
|
-
manifest['assets']['application.js'] = "#{digest}-#{File.basename(relative_path)}"
|
|
205
|
-
logical_path = 'application.js'
|
|
206
|
-
elsif relative_path.include?('application') && relative_path.end_with?('.css')
|
|
207
|
-
manifest['assets']['application.css'] = "#{digest}-#{File.basename(relative_path)}"
|
|
208
|
-
logical_path = 'application.css'
|
|
209
|
-
else
|
|
210
|
-
# For non-application files, still add to files but not to assets mapping
|
|
211
|
-
logical_path = relative_path
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Add file entry for all files
|
|
215
|
-
manifest['files']["#{digest}-#{File.basename(relative_path)}"] = {
|
|
216
|
-
'logical_path' => logical_path,
|
|
217
|
-
'pathname' => relative_path,
|
|
218
|
-
'digest' => digest,
|
|
219
|
-
'size' => File.size(file),
|
|
220
|
-
'mtime' => File.mtime(file).iso8601
|
|
221
|
-
}
|
|
222
|
-
end
|
|
223
233
|
|
|
224
|
-
# Write manifest for Sprockets (Rails < 7)
|
|
225
|
-
manifest_path = "#{outdir}/.sprockets-manifest-#{Digest::MD5.hexdigest(outdir)}.json"
|
|
226
|
-
File.write(manifest_path, JSON.pretty_generate(manifest))
|
|
227
|
-
Railpack.logger.debug "📄 Generated Sprockets manifest: #{manifest_path}"
|
|
228
|
-
end
|
|
229
234
|
end
|
|
230
235
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Railpack
|
|
6
|
+
# Manifest generation for different Rails asset pipelines.
|
|
7
|
+
#
|
|
8
|
+
# This module provides manifest generation for Propshaft (Rails 7+) and
|
|
9
|
+
# Sprockets (Rails < 7) asset pipelines, ensuring built assets are properly
|
|
10
|
+
# discoverable by Rails for serving and asset path helpers.
|
|
11
|
+
module Manifest
|
|
12
|
+
# Propshaft manifest generator for Rails 7+.
|
|
13
|
+
# Creates a simple JSON manifest mapping logical paths to physical files.
|
|
14
|
+
class Propshaft
|
|
15
|
+
def self.generate(config)
|
|
16
|
+
outdir = config['outdir']
|
|
17
|
+
return unless outdir && Dir.exist?(outdir)
|
|
18
|
+
|
|
19
|
+
manifest = {}
|
|
20
|
+
|
|
21
|
+
# Find built assets - Propshaft format
|
|
22
|
+
Dir.glob("#{outdir}/**/*.{js,css}").each do |file|
|
|
23
|
+
next unless File.file?(file)
|
|
24
|
+
relative_path = Pathname.new(file).relative_path_from(Pathname.new(outdir)).to_s
|
|
25
|
+
|
|
26
|
+
# Use relative path as logical path for Propshaft
|
|
27
|
+
logical_path = relative_path
|
|
28
|
+
manifest[logical_path] = {
|
|
29
|
+
'logical_path' => logical_path,
|
|
30
|
+
'pathname' => relative_path,
|
|
31
|
+
'digest' => Digest::MD5.file(file).hexdigest
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Write manifest for Propshaft (Rails 7+ default)
|
|
36
|
+
manifest_path = "#{outdir}/.manifest.json"
|
|
37
|
+
File.write(manifest_path, JSON.pretty_generate(manifest))
|
|
38
|
+
Railpack.logger.debug "📄 Generated Propshaft manifest: #{manifest_path}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sprockets manifest generator for Rails < 7.
|
|
43
|
+
# Creates a detailed manifest with digested filenames and asset mappings.
|
|
44
|
+
class Sprockets
|
|
45
|
+
def self.generate(config)
|
|
46
|
+
outdir = config['outdir']
|
|
47
|
+
return unless outdir && Dir.exist?(outdir)
|
|
48
|
+
|
|
49
|
+
manifest = {
|
|
50
|
+
'files' => {},
|
|
51
|
+
'assets' => {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Find built assets - Sprockets format
|
|
55
|
+
Dir.glob("#{outdir}/**/*.{js,css}").each do |file|
|
|
56
|
+
next unless File.file?(file)
|
|
57
|
+
relative_path = Pathname.new(file).relative_path_from(Pathname.new(outdir)).to_s
|
|
58
|
+
|
|
59
|
+
# Generate digest for Sprockets format
|
|
60
|
+
digest = Digest::MD5.file(file).hexdigest
|
|
61
|
+
logical_path = relative_path
|
|
62
|
+
|
|
63
|
+
# Map logical names (Sprockets style) - only for application files
|
|
64
|
+
if relative_path.include?('application') && relative_path.end_with?('.js')
|
|
65
|
+
manifest['assets']['application.js'] = "#{digest}-#{File.basename(relative_path)}"
|
|
66
|
+
logical_path = 'application.js'
|
|
67
|
+
elsif relative_path.include?('application') && relative_path.end_with?('.css')
|
|
68
|
+
manifest['assets']['application.css'] = "#{digest}-#{File.basename(relative_path)}"
|
|
69
|
+
logical_path = 'application.css'
|
|
70
|
+
else
|
|
71
|
+
# For non-application files, still add to files but not to assets mapping
|
|
72
|
+
logical_path = relative_path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Add file entry for all files
|
|
76
|
+
manifest['files']["#{digest}-#{File.basename(relative_path)}"] = {
|
|
77
|
+
'logical_path' => logical_path,
|
|
78
|
+
'pathname' => relative_path,
|
|
79
|
+
'digest' => digest,
|
|
80
|
+
'size' => File.size(file),
|
|
81
|
+
'mtime' => File.mtime(file).iso8601
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Write manifest for Sprockets (Rails < 7)
|
|
86
|
+
manifest_path = "#{outdir}/.sprockets-manifest-#{Digest::MD5.hexdigest(outdir)}.json"
|
|
87
|
+
File.write(manifest_path, JSON.pretty_generate(manifest))
|
|
88
|
+
Railpack.logger.debug "📄 Generated Sprockets manifest: #{manifest_path}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/railpack/version.rb
CHANGED
data/lib/railpack.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "railpack/bundlers/esbuild_bundler"
|
|
|
7
7
|
require_relative "railpack/bundlers/rollup_bundler"
|
|
8
8
|
require_relative "railpack/bundlers/webpack_bundler"
|
|
9
9
|
require_relative "railpack/config"
|
|
10
|
+
require_relative "railpack/manifest"
|
|
10
11
|
require_relative "railpack/manager"
|
|
11
12
|
|
|
12
13
|
module Railpack
|
data/test/manager_test.rb
CHANGED
|
@@ -54,8 +54,8 @@ class ManagerTest < Minitest::Test
|
|
|
54
54
|
manager = Railpack::Manager.new
|
|
55
55
|
|
|
56
56
|
size = manager.send(:calculate_bundle_size, config)
|
|
57
|
-
assert size.is_a?(
|
|
58
|
-
|
|
57
|
+
assert size.is_a?(String)
|
|
58
|
+
assert_match /\d+\.\d+ \w+/, size
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def test_manager_bundle_size_calculation_empty_directory
|
|
@@ -67,7 +67,7 @@ class ManagerTest < Minitest::Test
|
|
|
67
67
|
manager = Railpack::Manager.new
|
|
68
68
|
|
|
69
69
|
size = manager.send(:calculate_bundle_size, config)
|
|
70
|
-
assert_equal 0.0, size
|
|
70
|
+
assert_equal "0.0 B", size
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def test_manager_bundle_size_calculation_nonexistent_directory
|
|
@@ -232,7 +232,12 @@ class ManagerTest < Minitest::Test
|
|
|
232
232
|
config = { 'outdir' => outdir }
|
|
233
233
|
manager = Railpack::Manager.new
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
# Force Sprockets detection for this test
|
|
236
|
+
def manager.detect_asset_pipeline
|
|
237
|
+
:sprockets
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
manager.send(:generate_asset_manifest, config)
|
|
236
241
|
|
|
237
242
|
manifest_path = Dir.glob("#{outdir}/.sprockets-manifest-*.json").first
|
|
238
243
|
assert manifest_path
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railpack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.17
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 21tycoons LLC
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/railpack/bundlers/webpack_bundler.rb
|
|
46
46
|
- lib/railpack/config.rb
|
|
47
47
|
- lib/railpack/manager.rb
|
|
48
|
+
- lib/railpack/manifest.rb
|
|
48
49
|
- lib/railpack/version.rb
|
|
49
50
|
- lib/tasks/railpack.rake
|
|
50
51
|
- test/bundler_test.rb
|