skybolt 3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 730d6c870bd053421958e83321177a1f55f6d394eab14243c9db126d1177eb8b
4
+ data.tar.gz: '0883ef34760b719f36ec6e9221518f1034f3b9e3853f1bd54fb2432148adf446'
5
+ SHA512:
6
+ metadata.gz: ee08f62ab0bebbca2522022a22c52c1f3c00a8b30f27104c3e54487e963d4bf47ff03d23efac0ca34f5b825fba121090ebd2e1a8627be62305de5c400e586b49
7
+ data.tar.gz: af5ab159945553fbbf348e1e81ab706f4ee6d6d5c2a2a4c73d54ae843afec1aa4365bea3b36d1eaae8e7590f5e318e0597d721494b6f55c1d086758adf307ae6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jens Roland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # Skybolt Ruby
2
+
3
+ Ruby adapter for [Skybolt](https://github.com/JensRoland/skybolt) - High-performance asset caching for multi-page applications.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "skybolt"
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Prerequisites
20
+
21
+ 1. Install and configure the Vite plugin: `npm install @skybolt/vite-plugin`
22
+ 2. Build your project: `npm run build`
23
+ 3. Ensure `render-map.json` is generated in your build output
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require "skybolt"
29
+
30
+ sb = Skybolt::Renderer.new(
31
+ "public/dist/.skybolt/render-map.json",
32
+ cookies: request.cookies
33
+ )
34
+ ```
35
+
36
+ ```erb
37
+ <!DOCTYPE html>
38
+ <html>
39
+ <head>
40
+ <%= raw sb.css("src/css/critical.css") %>
41
+ <%= raw sb.launch_script %>
42
+ <%= raw sb.css("src/css/main.css") %>
43
+ </head>
44
+ <body>
45
+ <h1>Hello Skybolt!</h1>
46
+ <%= raw sb.script("src/js/app.js") %>
47
+ </body>
48
+ </html>
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `Skybolt::Renderer.new(render_map_path, cookies: nil, cdn_url: nil)`
54
+
55
+ Create a new Skybolt renderer.
56
+
57
+ - `render_map_path` - Path to `render-map.json` generated by Vite plugin
58
+ - `cookies:` - Cookie hash (defaults to `nil`)
59
+ - `cdn_url:` - Optional CDN URL prefix (e.g., `'https://cdn.example.com'`)
60
+
61
+ ```ruby
62
+ # Basic usage
63
+ sb = Skybolt::Renderer.new(
64
+ "public/dist/.skybolt/render-map.json",
65
+ cookies: request.cookies
66
+ )
67
+
68
+ # With CDN
69
+ sb = Skybolt::Renderer.new(
70
+ "public/dist/.skybolt/render-map.json",
71
+ cookies: request.cookies,
72
+ cdn_url: "https://cdn.example.com"
73
+ )
74
+ ```
75
+
76
+ ### `css(entry) -> String`
77
+
78
+ Render CSS asset.
79
+
80
+ - First visit: Inlines CSS with caching attributes
81
+ - Repeat visit: Outputs `<link>` tag (Service Worker serves from cache)
82
+
83
+ ```ruby
84
+ sb.css("src/css/main.css")
85
+ ```
86
+
87
+ ### `script(entry, is_module: true) -> String`
88
+
89
+ Render JavaScript asset.
90
+
91
+ - First visit: Inlines JS with caching attributes
92
+ - Repeat visit: Outputs `<script>` tag (Service Worker serves from cache)
93
+
94
+ ```ruby
95
+ # ES module (default)
96
+ sb.script("src/js/app.js")
97
+
98
+ # Classic script
99
+ sb.script("src/js/legacy.js", is_module: false)
100
+ ```
101
+
102
+ ### `launch_script -> String`
103
+
104
+ Render the Skybolt client launcher. Call once in `<head>` before other assets.
105
+
106
+ ```erb
107
+ <head>
108
+ <%= raw sb.launch_script %>
109
+ </head>
110
+ ```
111
+
112
+ ### `asset_url(entry) -> String?`
113
+
114
+ Get the URL for an asset (for manual use cases).
115
+
116
+ ```ruby
117
+ url = sb.asset_url("src/css/main.css")
118
+ # "/assets/main-Pw3rT8vL.css"
119
+ ```
120
+
121
+ ### `asset_hash(entry) -> String?`
122
+
123
+ Get the content hash for an asset.
124
+
125
+ ```ruby
126
+ hash = sb.asset_hash("src/css/main.css")
127
+ # "Pw3rT8vL"
128
+ ```
129
+
130
+ ### `preload(entry, as_type:, type: nil, crossorigin: nil, fetchpriority: nil) -> String`
131
+
132
+ Render preload link for critical resources like fonts and images.
133
+
134
+ ```ruby
135
+ # Preload hero image with high priority
136
+ sb.preload("images/hero.jpg", as_type: "image", fetchpriority: "high")
137
+
138
+ # Preload font
139
+ sb.preload("fonts/inter.woff2", as_type: "font", type: "font/woff2", crossorigin: "anonymous")
140
+
141
+ # Preload a Vite-built asset (resolved from render-map)
142
+ sb.preload("src/css/main.css", as_type: "style")
143
+ ```
144
+
145
+ Parameters:
146
+
147
+ - `entry` - Source file path or direct URL
148
+ - `as_type:` - Resource type (`'image'`, `'font'`, `'style'`, `'script'`, `'fetch'`)
149
+ - `type:` - MIME type (e.g., `'font/woff2'`, `'image/webp'`)
150
+ - `crossorigin:` - Crossorigin attribute (`'anonymous'`, `'use-credentials'`)
151
+ - `fetchpriority:` - Fetch priority (`'high'`, `'low'`, `'auto'`)
152
+
153
+ ## Service Worker Setup
154
+
155
+ The Service Worker must be served from your domain root. Configure your web server:
156
+
157
+ **Nginx:**
158
+
159
+ ```nginx
160
+ location = /skybolt-sw.js {
161
+ alias /path/to/public/dist/skybolt-sw.js;
162
+ }
163
+ ```
164
+
165
+ **Apache (.htaccess):**
166
+
167
+ ```apache
168
+ RewriteRule ^skybolt-sw\.js$ public/dist/skybolt-sw.js [L]
169
+ ```
170
+
171
+ ## Framework Integration
172
+
173
+ ### Rails
174
+
175
+ ```ruby
176
+ # app/helpers/skybolt_helper.rb
177
+ module SkyboltHelper
178
+ def skybolt
179
+ @skybolt ||= Skybolt::Renderer.new(
180
+ Rails.root.join("public/dist/.skybolt/render-map.json").to_s,
181
+ cookies: cookies.to_h
182
+ )
183
+ end
184
+ end
185
+ ```
186
+
187
+ ```erb
188
+ <%# app/views/layouts/application.html.erb %>
189
+ <!DOCTYPE html>
190
+ <html>
191
+ <head>
192
+ <%= raw skybolt.css("app/assets/stylesheets/critical.css") %>
193
+ <%= raw skybolt.launch_script %>
194
+ <%= raw skybolt.css("app/assets/stylesheets/application.css") %>
195
+ </head>
196
+ <body>
197
+ <%= yield %>
198
+ <%= raw skybolt.script("app/javascript/application.js") %>
199
+ </body>
200
+ </html>
201
+ ```
202
+
203
+ ### Sinatra
204
+
205
+ ```ruby
206
+ require "sinatra"
207
+ require "skybolt"
208
+
209
+ helpers do
210
+ def skybolt
211
+ @skybolt ||= Skybolt::Renderer.new(
212
+ "public/dist/.skybolt/render-map.json",
213
+ cookies: request.cookies
214
+ )
215
+ end
216
+ end
217
+
218
+ get "/" do
219
+ erb :index
220
+ end
221
+ ```
222
+
223
+ ```erb
224
+ <%# views/index.erb %>
225
+ <!DOCTYPE html>
226
+ <html>
227
+ <head>
228
+ <%= raw skybolt.css("src/css/critical.css") %>
229
+ <%= raw skybolt.launch_script %>
230
+ <%= raw skybolt.css("src/css/main.css") %>
231
+ </head>
232
+ <body>
233
+ <h1>Hello Skybolt!</h1>
234
+ <%= raw skybolt.script("src/js/app.js") %>
235
+ </body>
236
+ </html>
237
+ ```
238
+
239
+ ### Hanami
240
+
241
+ ```ruby
242
+ # app/actions/home/index.rb
243
+ module MyApp
244
+ module Actions
245
+ module Home
246
+ class Index < MyApp::Action
247
+ def handle(request, response)
248
+ sb = Skybolt::Renderer.new(
249
+ "public/dist/.skybolt/render-map.json",
250
+ cookies: request.cookies
251
+ )
252
+ response.render(view, sb: sb)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ ```
259
+
260
+ ## Requirements
261
+
262
+ - Ruby 3.0+
263
+ - Vite with `@skybolt/vite-plugin`
264
+
265
+ ## Publishing
266
+
267
+ This package is maintained in the [Skybolt monorepo](https://github.com/JensRoland/skybolt) and automatically synced to [skybolt-ruby](https://github.com/JensRoland/skybolt-ruby).
268
+
269
+ To publish a new version, run one command from the `packages/ruby` directory:
270
+
271
+ ```sh
272
+ ./scripts/release.sh patch # 3.1.0 → 3.1.1
273
+ ./scripts/release.sh minor # 3.1.0 → 3.2.0
274
+ ./scripts/release.sh major # 3.1.0 → 4.0.0
275
+ ```
276
+
277
+ This automatically:
278
+
279
+ 1. Bumps the version in `VERSION`, `skybolt.gemspec`, and `lib/skybolt/version.rb`
280
+ 2. Commits and pushes to the monorepo
281
+ 3. Sync workflow pushes changes to the split repo
282
+ 4. `tag-version.yml` in the split repo creates the `v*` tag
283
+ 5. `publish.yml` builds and publishes to RubyGems using trusted publishing (OIDC)
284
+
285
+ ## License
286
+
287
+ MIT
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "cgi"
5
+ require "uri"
6
+
7
+ module Skybolt
8
+ # Skybolt asset renderer.
9
+ #
10
+ # Reads the render-map.json generated by @skybolt/vite-plugin and outputs
11
+ # optimized HTML tags with intelligent caching via Service Workers.
12
+ class Renderer
13
+ # @param render_map_path [String] Path to render-map.json generated by Vite plugin
14
+ # @param cookies [Hash, nil] Cookie hash (defaults to empty hash)
15
+ # @param cdn_url [String, nil] Optional CDN URL prefix
16
+ def initialize(render_map_path, cookies: nil, cdn_url: nil)
17
+ json = File.read(render_map_path)
18
+ @map = JSON.parse(json)
19
+ @client_cache = parse_cookie((cookies || {})["sb_assets"] || "")
20
+ @cdn_url = cdn_url&.chomp("/")
21
+ end
22
+
23
+ # Render CSS asset.
24
+ #
25
+ # On first visit: inlines CSS with sb-* attributes for caching
26
+ # On repeat visit: outputs <link> tag (Service Worker serves from cache)
27
+ #
28
+ # @param entry [String] Source file path (e.g., 'src/css/main.css')
29
+ # @return [String] HTML string
30
+ def css(entry)
31
+ asset = @map.dig("assets", entry)
32
+ return comment("Skybolt: asset not found: #{entry}") if asset.nil?
33
+
34
+ url = resolve_url(asset["url"])
35
+
36
+ # Client has current version - external link (SW serves from cache)
37
+ if cached?(entry, asset["hash"])
38
+ return %(<link rel="stylesheet" href="#{esc(url)}">)
39
+ end
40
+
41
+ # First visit - inline with cache attributes
42
+ %(<style sb-asset="#{esc(entry)}:#{esc(asset["hash"])}" sb-url="#{esc(url)}">#{asset["content"]}</style>)
43
+ end
44
+
45
+ # Render JavaScript asset.
46
+ #
47
+ # On first visit: inlines JS with sb-* attributes for caching
48
+ # On repeat visit: outputs <script> tag (Service Worker serves from cache)
49
+ #
50
+ # @param entry [String] Source file path (e.g., 'src/js/app.js')
51
+ # @param is_module [Boolean] Whether to load as ES module (default: true)
52
+ # @return [String] HTML string
53
+ def script(entry, is_module: true)
54
+ asset = @map.dig("assets", entry)
55
+ return comment("Skybolt: asset not found: #{entry}") if asset.nil?
56
+
57
+ url = resolve_url(asset["url"])
58
+ type_attr = is_module ? ' type="module"' : ""
59
+
60
+ # Client has current version - external script (SW serves from cache)
61
+ if cached?(entry, asset["hash"])
62
+ return %(<script#{type_attr} src="#{esc(url)}"></script>)
63
+ end
64
+
65
+ # First visit - inline with cache attributes
66
+ %(<script#{type_attr} sb-asset="#{esc(entry)}:#{esc(asset["hash"])}" sb-url="#{esc(url)}">#{asset["content"]}</script>)
67
+ end
68
+
69
+ # Render preload link for critical resources.
70
+ #
71
+ # Use this for fonts, images, or other resources that should load early.
72
+ # Preloaded resources are not cached by Skybolt's Service Worker.
73
+ #
74
+ # @param entry [String] Source file path or direct URL
75
+ # @param as_type [String] Resource type ('image', 'font', 'style', 'script', 'fetch')
76
+ # @param type [String, nil] MIME type (e.g., 'font/woff2', 'image/webp')
77
+ # @param crossorigin [String, nil] Crossorigin attribute ('anonymous', 'use-credentials')
78
+ # @param fetchpriority [String, nil] Fetch priority ('high', 'low', 'auto')
79
+ # @return [String] HTML string
80
+ def preload(entry, as_type:, type: nil, crossorigin: nil, fetchpriority: nil)
81
+ # Try to resolve from assets, fall back to using entry as URL
82
+ url = asset_url(entry) || entry
83
+ url = resolve_url(url)
84
+
85
+ attrs = {
86
+ rel: "preload",
87
+ href: url,
88
+ as: as_type
89
+ }
90
+ attrs[:type] = type if type
91
+ attrs[:crossorigin] = crossorigin if crossorigin
92
+ attrs[:fetchpriority] = fetchpriority if fetchpriority
93
+
94
+ build_tag("link", attrs)
95
+ end
96
+
97
+ # Render the Skybolt client launcher.
98
+ #
99
+ # Call this once in <head> before other Skybolt assets.
100
+ # Outputs config meta tag and client script.
101
+ #
102
+ # @return [String] HTML string
103
+ def launch_script
104
+ sw_path = @map.dig("serviceWorker", "path") || "/skybolt-sw.js"
105
+ config = { swPath: sw_path }.to_json
106
+
107
+ %(<meta name="skybolt-config" content="#{esc(config)}">\n<script type="module">#{@map.dig("client", "script")}</script>)
108
+ end
109
+
110
+ # Get URL for an asset (for manual use cases).
111
+ #
112
+ # @param entry [String] Source file path
113
+ # @return [String, nil] Asset URL or nil if not found
114
+ def asset_url(entry)
115
+ url = @map.dig("assets", entry, "url")
116
+ url ? resolve_url(url) : nil
117
+ end
118
+
119
+ # Get hash for an asset (for manual use cases).
120
+ #
121
+ # @param entry [String] Source file path
122
+ # @return [String, nil] Asset hash or nil if not found
123
+ def asset_hash(entry)
124
+ @map.dig("assets", entry, "hash")
125
+ end
126
+
127
+ private
128
+
129
+ def resolve_url(url)
130
+ return url if @cdn_url.nil?
131
+ return url if url.start_with?("http://", "https://", "//")
132
+
133
+ "#{@cdn_url}#{url}"
134
+ end
135
+
136
+ def cached?(entry, hash)
137
+ @client_cache[entry] == hash
138
+ end
139
+
140
+ def parse_cookie(cookie)
141
+ return {} if cookie.empty?
142
+
143
+ decoded = URI.decode_www_form_component(cookie)
144
+ cache = {}
145
+
146
+ decoded.split(",").each do |pair|
147
+ # Find last colon (hash doesn't contain colons, but paths might)
148
+ colon_pos = pair.rindex(":")
149
+ next if colon_pos.nil?
150
+
151
+ name = pair[0...colon_pos]
152
+ hash = pair[(colon_pos + 1)..]
153
+ cache[name] = hash
154
+ end
155
+
156
+ cache
157
+ end
158
+
159
+ def build_tag(tag, attrs)
160
+ attr_str = attrs.map { |k, v| %(#{k}="#{esc(v)}") }.join(" ")
161
+ "<#{tag} #{attr_str}>"
162
+ end
163
+
164
+ def esc(value)
165
+ CGI.escapeHTML(value.to_s)
166
+ end
167
+
168
+ def comment(text)
169
+ "<!-- #{esc(text)} -->"
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skybolt
4
+ VERSION = "3.2.0"
5
+ end
data/lib/skybolt.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "skybolt/version"
4
+ require_relative "skybolt/renderer"
5
+
6
+ module Skybolt
7
+ class Error < StandardError; end
8
+ end
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skybolt
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Jens Roland
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Reads the render-map.json generated by @skybolt/vite-plugin and outputs
14
+ optimized HTML tags with intelligent caching via Service Workers.
15
+ email:
16
+ - mail@jensroland.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - lib/skybolt.rb
24
+ - lib/skybolt/renderer.rb
25
+ - lib/skybolt/version.rb
26
+ homepage: https://github.com/JensRoland/skybolt-ruby
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ homepage_uri: https://github.com/JensRoland/skybolt-ruby
31
+ source_code_uri: https://github.com/JensRoland/skybolt-ruby
32
+ changelog_uri: https://github.com/JensRoland/skybolt-ruby/blob/main/CHANGELOG.md
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.4.19
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: High-performance asset caching for multi-page applications
52
+ test_files: []