rhales 0.5.4 → 0.6.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: ba52e82c80de6bba26167aaf61b424d72a03fc195ec69be7e38dbd17422cf064
4
- data.tar.gz: d87dc62997bf10cfde421bb93e7d1025b2374e9cd53a2280acd37c157d553227
3
+ metadata.gz: 249890f733fb9b88bbabe22bcc327aa5d0422c00ea595e4b67751e335e790feb
4
+ data.tar.gz: 69259f0ab5cc091cc2c4fab33e3eee13859e28ef789e522119e6702f695155ab
5
5
  SHA512:
6
- metadata.gz: 8368580c220c662432ede3e323b9460b6edba4ff5012c57fbbe0b4760572d063aed07225a4e9b29d542d00f169c7ccdab9ddf7c1aa742b67d0a80c438ad91b18
7
- data.tar.gz: 2ff74a6825fda46507e849dfc19139c7084be205024a03d2b0fd5f8d930d71366ce5c3407fbeeb77acf133bdd5486b17dbc5b068780ec169a1ee29c15669f1e2
6
+ metadata.gz: c424d4d348c48686ddb60fe068d2839627c552d94a03a507b9cf54f1d39d11dc4d18aaefccc8731ec0aa8fb79d72ef18ea838460d0e36f5d69b5575d7d15ac3d
7
+ data.tar.gz: 65139df9054a39981ca7f6fd51818544aca039847d4a1d60521e879c2d542f3ca1f2ca9bbeb030154cc735404882d5ca0ede2fcabe64ef55a8194c622518c963
@@ -26,7 +26,7 @@ jobs:
26
26
  strategy:
27
27
  fail-fast: true
28
28
  matrix:
29
- ruby: ['3.4', '3.5']
29
+ ruby: ['3.2', '3.3', '3.4', '3.5']
30
30
 
31
31
  steps:
32
32
  - uses: actions/checkout@v4
@@ -50,7 +50,7 @@ jobs:
50
50
  strategy:
51
51
  fail-fast: true
52
52
  matrix:
53
- ruby: ['3.4', '3.5']
53
+ ruby: ['3.2', '3.3', '3.4', '3.5']
54
54
 
55
55
  steps:
56
56
  - uses: actions/checkout@v4
@@ -77,7 +77,7 @@ jobs:
77
77
  strategy:
78
78
  fail-fast: false
79
79
  matrix:
80
- ruby: ['3.4', '3.5']
80
+ ruby: ['3.2', '3.3', '3.4', '3.5']
81
81
 
82
82
  steps:
83
83
  - uses: actions/checkout@v4
@@ -99,7 +99,7 @@ jobs:
99
99
  strategy:
100
100
  fail-fast: false
101
101
  matrix:
102
- ruby: ['3.4', '3.5']
102
+ ruby: ['3.2', '3.3', '3.4', '3.5']
103
103
 
104
104
  steps:
105
105
  - uses: actions/checkout@v4
@@ -0,0 +1,158 @@
1
+ # Release Gem Workflow
2
+ #
3
+ # Automatically builds and publishes the rhales gem to RubyGems.org when a
4
+ # GitHub release is published with a semantic version tag (vMAJOR.MINOR.PATCH,
5
+ # e.g. v0.6.1, v1.2.3).
6
+ #
7
+ # Follows the canonical RubyGems Trusted Publishing workflow:
8
+ # https://guides.rubygems.org/trusted-publishing/
9
+ #
10
+ # ============================================================================
11
+ # SETUP INSTRUCTIONS (one-time)
12
+ # ============================================================================
13
+ #
14
+ # This workflow uses RubyGems Trusted Publishing (OIDC). No long-lived API
15
+ # key needs to be stored in GitHub.
16
+ #
17
+ # ----------------------------------------------------------------------------
18
+ # 1. Configure the trusted publisher on RubyGems.org
19
+ # ----------------------------------------------------------------------------
20
+ #
21
+ # a. Sign in to https://rubygems.org and enable MFA on your account
22
+ # (required for trusted publishing).
23
+ #
24
+ # b. Open the gem's trusted publisher page (works for both existing gems
25
+ # and the first-ever publish):
26
+ # Existing gem: https://rubygems.org/gems/rhales/trusted_publishers
27
+ # First publish: https://rubygems.org/profile/oidc/pending_trusted_publishers/new
28
+ #
29
+ # c. Click "Create" and fill in:
30
+ # Publisher type: GitHub Actions
31
+ # Repository owner: onetimesecret
32
+ # Repository name: rhales
33
+ # Workflow filename: release-gem.yml
34
+ # Environment: rubygems.org (must match `environment.name` below)
35
+ #
36
+ # d. Save. RubyGems will now accept short-lived OIDC tokens issued by this
37
+ # workflow running in this repository.
38
+ #
39
+ # ----------------------------------------------------------------------------
40
+ # 2. Configure the GitHub environment
41
+ # ----------------------------------------------------------------------------
42
+ #
43
+ # a. In GitHub: Settings -> Environments -> New environment
44
+ # Name: `rubygems.org` (must match `environment.name` below)
45
+ #
46
+ # b. Under "Deployment branches and tags", restrict to tags matching:
47
+ # v*.*.*
48
+ # This prevents the environment (and its OIDC token) from being used
49
+ # from any branch or non-release tag.
50
+ #
51
+ # c. Optional but recommended: add required reviewers to gate publishes
52
+ # behind manual approval.
53
+ #
54
+ # No GitHub secrets are required - OIDC handles authentication.
55
+ #
56
+ # ----------------------------------------------------------------------------
57
+ # 3. Cutting a release
58
+ # ----------------------------------------------------------------------------
59
+ #
60
+ # a. Bump Rhales::VERSION in lib/rhales/version.rb (semver: MAJOR.MINOR.PATCH).
61
+ # b. Update CHANGELOG.md and commit on main.
62
+ # c. On GitHub, draft a Release with tag `vX.Y.Z` (matching the constant)
63
+ # and publish it. This workflow runs automatically: it verifies the tag
64
+ # matches the gemspec version, builds the gem, and pushes it.
65
+ #
66
+ # ----------------------------------------------------------------------------
67
+ # Fallback: API key (only if Trusted Publishing isn't an option)
68
+ # ----------------------------------------------------------------------------
69
+ #
70
+ # 1. Create an API key at https://rubygems.org/profile/api_keys with scope
71
+ # "Push rubygem" restricted to the `rhales` gem.
72
+ # 2. Store it as a repo secret named `RUBYGEMS_API_KEY`.
73
+ # 3. Drop the `id-token: write` permission and `environment:` block, and
74
+ # replace the `rubygems/release-gem` step with:
75
+ #
76
+ # - name: Publish to RubyGems
77
+ # env:
78
+ # GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
79
+ # run: gem push rhales-*.gem
80
+ #
81
+ # ----------------------------------------------------------------------------
82
+ # Action provenance (SHA pins)
83
+ # ----------------------------------------------------------------------------
84
+ #
85
+ # All third-party actions below are pinned to a full commit SHA, per GitHub's
86
+ # secure-use guidance for release-critical workflows. The accompanying
87
+ # comment records the tag the SHA was resolved from so renovate/dependabot
88
+ # can keep them current.
89
+ #
90
+ # actions/checkout - official GitHub action
91
+ # ruby/setup-ruby - official Ruby org action (GitHub-verified creator)
92
+ # rubygems/release-gem - official RubyGems action for trusted publishing
93
+ #
94
+ # ============================================================================
95
+
96
+ name: Release Gem
97
+
98
+ on:
99
+ release:
100
+ types: [published]
101
+
102
+ # Coarse default. Each job re-declares the narrowest permissions it needs.
103
+ permissions:
104
+ contents: read
105
+
106
+ # Don't allow two release runs to race - one published tag, one publish.
107
+ concurrency:
108
+ group: release-gem-${{ github.event.release.tag_name }}
109
+ cancel-in-progress: false
110
+
111
+ jobs:
112
+ release:
113
+ name: Build and push gem
114
+ runs-on: ubuntu-latest
115
+ timeout-minutes: 10
116
+
117
+ environment:
118
+ name: rubygems.org
119
+ url: https://rubygems.org/gems/rhales
120
+
121
+ permissions:
122
+ contents: write # rubygems/release-gem attaches built .gem to the GitHub release
123
+ id-token: write # OIDC token for RubyGems Trusted Publishing
124
+
125
+ steps:
126
+ - name: Checkout
127
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
128
+ with:
129
+ persist-credentials: false
130
+
131
+ - name: Set up Ruby
132
+ uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
133
+ with:
134
+ bundler-cache: true
135
+ ruby-version: ruby # latest stable Ruby installed by setup-ruby
136
+
137
+ - name: Verify release tag matches Rhales::VERSION
138
+ env:
139
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
140
+ run: |
141
+ set -euo pipefail
142
+ case "${RELEASE_TAG}" in
143
+ v[0-9]*.[0-9]*.[0-9]*) ;;
144
+ *)
145
+ echo "Release tag '${RELEASE_TAG}' is not a vMAJOR.MINOR.PATCH semver tag." >&2
146
+ exit 1
147
+ ;;
148
+ esac
149
+ tag_version="${RELEASE_TAG#v}"
150
+ gem_version="$(ruby -r ./lib/rhales/version.rb -e 'print Rhales::VERSION')"
151
+ if [ "${tag_version}" != "${gem_version}" ]; then
152
+ echo "Release tag ${RELEASE_TAG} (${tag_version}) != Rhales::VERSION (${gem_version})." >&2
153
+ exit 1
154
+ fi
155
+ echo "Releasing rhales ${gem_version} from tag ${RELEASE_TAG}"
156
+
157
+ - name: Build and push gem to RubyGems
158
+ uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0
@@ -48,7 +48,7 @@ jobs:
48
48
  strategy:
49
49
  fail-fast: true
50
50
  matrix:
51
- ruby: ['3.4', '3.5']
51
+ ruby: ['3.2', '3.3', '3.4', '3.5']
52
52
  continue-on-error: [true]
53
53
 
54
54
  steps:
data/.rubocop.yml CHANGED
@@ -17,7 +17,7 @@ AllCops:
17
17
  DisabledByDefault: false # flip to true for a good autocorrect time
18
18
  UseCache: true
19
19
  MaxFilesInCache: 1000
20
- TargetRubyVersion: 3.4
20
+ TargetRubyVersion: 3.2
21
21
  Exclude:
22
22
  - 'bin/bundle'
23
23
  - 'node_modules/**/*'
data/CHANGELOG.md CHANGED
@@ -7,7 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.2] - 2026-05-25
11
+
10
12
  ### Added
13
+ - **Automated gem release workflow** (`.github/workflows/release-gem.yml`):
14
+ builds and publishes the gem to RubyGems.org via Trusted Publishing
15
+ (OIDC) whenever a GitHub Release is published with a `vMAJOR.MINOR.PATCH`
16
+ tag. Verifies the tag matches `Rhales::VERSION` before pushing.
17
+
18
+ ### Changed
19
+ - **Dependencies**: bumped `unicode-emoji` from 4.0.4 to 4.2.0 and
20
+ `unicode-display_width` from 3.1.4 to 3.2.0. The previous
21
+ `unicode-emoji` cap of `< Ruby 4.0` broke `bundle install` on Ruby 4.x;
22
+ 4.2.0 lifts that cap.
23
+ - **Gemfile.lock**: platform list normalized via
24
+ `bundle lock --normalize-platforms` so platform-specific gems no longer
25
+ trigger setup-ruby warnings on CI.
26
+
27
+ ## [0.6.1] - 2026-05-25
28
+
29
+ ### Added
30
+ - **Server-rendered random tokens example** (`examples/token-loader.rue`):
31
+ pattern-teaching example showing how to generate per-request data with
32
+ `SecureRandom.hex` in a Ruby view model, validate a nested
33
+ `z.array(z.object(...))` shape, and walk it with nested `{{#each}}` blocks
34
+ that fall through to the current outer item without explicit bindings. The
35
+ original loader-animation use case is documented as a CSS-only follow-on in
36
+ the `<logic>` block. (#49)
37
+
38
+ ### Changed
39
+ - **Minimum Ruby version lowered from 3.4 to 3.2** - broader compatibility with stable Ruby releases; CI now exercises 3.2, 3.3, 3.4, and 3.5
40
+
41
+ ### Fixed
42
+ - `{{#each}}` block variable `@last` now correctly returns `true` for the final
43
+ iteration instead of always `false`. Enables comma-separated output via
44
+ `{{#unless @last}},{{/unless}}` and similar patterns. `EachContext` now
45
+ accepts and stores the collection's total length.
46
+
47
+ ## [0.6.0] - 2026-03-21
48
+
49
+ ### Added
50
+ - **External Schema References**: Schema definitions can now reference external TypeScript/JavaScript files via the `src` attribute
51
+ - Enables single-source-of-truth patterns where TypeScript schemas drive both frontend types and Rhales validation
52
+ - Path resolution relative to template file with security checks to prevent path traversal
53
+ - Rake task output now shows inline vs external schema sources
54
+ - Example: `<schema src="schemas/user.schema.ts" lang="js-zod" window="__USER__">`
55
+ - **Multi-directory Schema Search**: New `schema_search_paths` configuration option
56
+ - Allows searching multiple directories for external schema files
57
+ - Resolution order: template-relative first, then search paths in order
58
+ - Security checks apply to all configured paths
59
+ - **tsx Import Mode**: New bundling mode for external schemas with imports
60
+ - `schema_use_tsx_import = true` enables esbuild bundling
61
+ - `schema_tsconfig_path` allows custom TypeScript configuration
62
+ - Externalizes zod to prevent dual-instance issues
63
+ - Cross-platform file:// URL support for Windows ESM compatibility
11
64
  - **Production Logging**: Structured logging via `Rhales.logger=` for security auditing and debugging
12
65
  - View rendering events with template details, timing, and hydration size
13
66
  - Schema validation warnings for production debugging (missing/extra keys)
@@ -23,16 +76,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
76
  - Comprehensive test coverage for collision detection and merge strategies
24
77
 
25
78
  ### Changed
79
+ - **Ruby 3.4+ required** (was 3.3.4) - aligns with current LTS ecosystem; no Ruby 3.3 features relied upon, but 3.4 is recommended for YJIT improvements and json_schemer performance
80
+ - Updated zod to 4.3.6
81
+ - Relaxed json_schemer dependency from ~> 2.3 to ~> 2
26
82
  - Replaced `json-schema` gem with `json_schemer` for better JSON Schema Draft 2020-12 support
27
83
  - Improved validation error messages with more structured output from json_schemer
28
84
  - Validation performance improved to <0.05ms average (was ~2ms with json-schema)
29
85
 
86
+ ### Removed
87
+ - Unused `HydrationRegistry.clear!` method
88
+
30
89
  ### Security
31
90
  - Window collision detection prevents accidental data exposure by making overwrites explicit
32
91
  - All merge operations happen client-side after server-side interpolation and JSON serialization
33
92
  - Request-scoped registry prevents cross-request data leakage
34
93
 
35
- ## [0.1.0] - 2024-01-XX
94
+ ## [0.1.0] - 2025-07-21
36
95
 
37
96
  ### Added
38
97
  - Initial release of Rhales
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rhales (0.5.3)
5
- json_schemer (~> 2.3)
4
+ rhales (0.6.2)
5
+ json_schemer (~> 2)
6
6
  logger
7
7
  tilt (~> 2)
8
8
 
@@ -153,14 +153,14 @@ GEM
153
153
  prettier_print (>= 1.2.0)
154
154
  tilt (2.6.1)
155
155
  tsort (0.2.0)
156
- unicode-display_width (3.1.4)
157
- unicode-emoji (~> 4.0, >= 4.0.4)
158
- unicode-emoji (4.0.4)
156
+ unicode-display_width (3.2.0)
157
+ unicode-emoji (~> 4.1)
158
+ unicode-emoji (4.2.0)
159
159
  yard (0.9.37)
160
160
  zeitwerk (2.7.3)
161
161
 
162
162
  PLATFORMS
163
- arm64-darwin-24
163
+ arm64-darwin
164
164
  ruby
165
165
 
166
166
  DEPENDENCIES
@@ -186,4 +186,4 @@ DEPENDENCIES
186
186
  yard (~> 0.9)
187
187
 
188
188
  BUNDLED WITH
189
- 2.6.6
189
+ 2.7.2
data/README.md CHANGED
@@ -1,20 +1,20 @@
1
1
  # Rhales - Ruby Single File Components
2
2
 
3
3
  > [!CAUTION]
4
- > **Early Development Release** - Rhales is in active development (v0.5). The API underwent breaking changes from v0.4. While functional and tested, it's recommended for experimental use and contributions. Please report issues and provide feedback through GitHub.
4
+ > **Early Development Release** - Rhales is in active development (v0.6). The API underwent breaking changes from v0.4. While functional and tested, it's recommended for experimental use and contributions. Please report issues and provide feedback through GitHub.
5
5
 
6
6
  Rhales is a **type-safe contract enforcement framework** for server-rendered pages with client-side data hydration. It uses `.rue` files (Ruby Single File Components) that combine Zod v4 schemas, Handlebars templates, and documentation into a single contract-first format.
7
7
 
8
8
  **About the name:** It all started with a simple mustache template many years ago. Mustache's successor, "Handlebars," is a visual analog for a mustache. "Two Whales Kissing" is another visual analog for a mustache, and since we're working with Ruby, we call it "Rhales" (Ruby + Whales). It's a perfect name with absolutely no ambiguity or risk of confusion.
9
9
 
10
- ## What's New in v0.5
10
+ ## What's New in v0.6
11
11
 
12
- - ✅ **Schema-First Design**: Replaced `<data>` sections with Zod v4 `<schema>` sections
13
- - ✅ **Type Safety**: Contract enforcement between backend and frontend
14
- - ✅ **Simplified API**: Removed deprecated parameters (`sess`, `cust`, `props:`, `app_data:`)
15
- - ✅ **Clear Context Layers**: Renamed `app` → `request` for clarity
16
- - ✅ **Schema Tooling**: Rake tasks for schema generation and validation
17
- - **100% Migration**: All demo templates use schemas
12
+ - ✅ **External Schema References**: Reference TypeScript schema files via `src` attribute for single-source-of-truth patterns
13
+ - ✅ **Multi-directory Search**: Configure `schema_search_paths` to search multiple directories for shared schemas
14
+ - ✅ **tsx Import Mode**: Bundle external schemas with imports via esbuild (`schema_use_tsx_import`)
15
+ - ✅ **Ruby 3.2+ Supported**: Minimum Ruby version
16
+
17
+ **v0.5 features:** Schema-first design, type safety, simplified API, clear context layers, schema tooling.
18
18
 
19
19
  **Breaking changes from v0.4:** See [Migration Guide](#migration-from-v04-to-v05) below.
20
20
 
@@ -134,6 +134,65 @@ const schema = z.object({
134
134
  | `version` | No | Schema version | `"2"` |
135
135
  | `envelope` | No | Response wrapper type | `"SuccessEnvelope"` |
136
136
  | `layout` | No | Layout template reference | `"layouts/main"` |
137
+ | `src` | No | External schema file path | `"schemas/user.schema.ts"` |
138
+
139
+ ### External Schema References
140
+
141
+ Instead of defining schemas inline, you can reference external TypeScript/JavaScript files. This enables single-source-of-truth patterns where the same schema file drives both frontend TypeScript types (via `z.infer<>`) and Rhales validation.
142
+
143
+ ```xml
144
+ <!-- templates/dashboard.rue -->
145
+ <schema src="schemas/dashboard.schema.ts" lang="js-zod" window="__DASHBOARD__">
146
+ </schema>
147
+
148
+ <template>
149
+ <div>{{user.name}}</div>
150
+ </template>
151
+ ```
152
+
153
+ ```typescript
154
+ // templates/schemas/dashboard.schema.ts
155
+ import { z } from 'zod';
156
+
157
+ const schema = z.object({
158
+ user: z.object({
159
+ name: z.string(),
160
+ email: z.string().email()
161
+ }),
162
+ items: z.array(z.string())
163
+ });
164
+
165
+ export default schema;
166
+
167
+ // TypeScript frontend can import and use: z.infer<typeof schema>
168
+ ```
169
+
170
+ The `src` path is resolved relative to the template file. Security checks prevent path traversal outside the templates directory.
171
+
172
+ #### Multi-directory Search
173
+
174
+ Configure additional directories to search for shared schema files:
175
+
176
+ ```ruby
177
+ Rhales.configure do |config|
178
+ config.schema_search_paths = ['./shared/schemas', './lib/schemas']
179
+ end
180
+ ```
181
+
182
+ Resolution order: template-relative first, then search paths in order.
183
+
184
+ #### tsx Import Mode
185
+
186
+ For external schemas that import other modules, enable esbuild bundling:
187
+
188
+ ```ruby
189
+ Rhales.configure do |config|
190
+ config.schema_use_tsx_import = true
191
+ config.schema_tsconfig_path = './tsconfig.json' # optional
192
+ end
193
+ ```
194
+
195
+ This bundles the schema with all its imports while externalizing zod to prevent dual-instance issues.
137
196
 
138
197
  ### Zod Schema Examples
139
198
 
@@ -0,0 +1,119 @@
1
+ <!-- examples/token-loader.rue -->
2
+
3
+ <!--
4
+ Example: Server-Rendered Random Tokens with Nested Iteration
5
+
6
+ Demonstrates four rhales patterns in one small template:
7
+
8
+ 1. Generating per-request random data on the server (SecureRandom.hex)
9
+ and passing it to the template via the client: context, instead of
10
+ relying on client JavaScript to fill the page in.
11
+
12
+ 2. Validating a nested-array-of-objects shape with
13
+ z.array(z.object(...)) in the <schema> block.
14
+
15
+ 3. Iterating with {{#each}} over a top-level array.
16
+
17
+ 4. Iterating again inside the outer block to walk a nested array.
18
+ The inner {{#each}} resolves its variable name against the current
19
+ outer item first, so no explicit binding (`as |cell|`) or path
20
+ prefix (`this.glyphs`) is needed.
21
+
22
+ Each render produces a different output. The most common use case is
23
+ a pre-boot loader animated with CSS (see the "Use as a loader" note
24
+ below), but the same shape applies anywhere you want server-generated
25
+ per-request decorative data: identicons, correlation IDs displayed on
26
+ error pages, example IDs in onboarding flows, etc.
27
+ -->
28
+
29
+ <schema lang="js-zod" window="loaderTokens">
30
+ const schema = z.object({
31
+ cells: z.array(z.object({
32
+ glyphs: z.array(z.string().length(1)).length(17)
33
+ })).length(5)
34
+ });
35
+ </schema>
36
+
37
+ <template>
38
+ <style nonce="{{nonce}}">
39
+ .tokens {
40
+ display: inline-flex;
41
+ gap: .3rem;
42
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
43
+ letter-spacing: 0.05em;
44
+ }
45
+ </style>
46
+
47
+ <div class="tokens">
48
+ {{#each cells}}<span class="cell">{{#each glyphs}}{{.}}{{/each}}</span>{{/each}}
49
+ </div>
50
+ </template>
51
+
52
+ <logic>
53
+ # Server-Rendered Random Tokens
54
+ #
55
+ # Backend Usage (Ruby):
56
+ #
57
+ # require 'securerandom'
58
+ #
59
+ # cells = Array.new(5) {
60
+ # { glyphs: Array.new(17) { SecureRandom.hex(1)[0] } }
61
+ # }
62
+ #
63
+ # view = Rhales::View.new(
64
+ # request,
65
+ # client: { cells: cells },
66
+ # server: { pageTitle: 'Loading...' }
67
+ # )
68
+ #
69
+ # html = view.render('token-loader')
70
+ #
71
+ # Why these patterns:
72
+ #
73
+ # 1. Randomness lives in the view model, not the template. Rhales
74
+ # templates don't expose Random.new or SecureRandom directly --
75
+ # generate the data in Ruby and hand it to the template via the
76
+ # client: context. Templates stay pure-render and the data flow
77
+ # is explicit.
78
+ #
79
+ # 2. CSPRNG over Kernel#rand. Even for decorative output, SecureRandom
80
+ # signals intent and avoids questions later about why a templating
81
+ # layer is pulling from a seeded RNG.
82
+ #
83
+ # 3. Fresh data per cell, not a shared pool. Generating 5 x 17 = 85
84
+ # fresh glyphs (vs. 17 glyphs shuffled five ways) avoids visible
85
+ # rhyming between adjacent columns.
86
+ #
87
+ # 4. Nested {{#each}} resolves against the current outer item. Inside
88
+ # {{#each cells}}, the inner {{#each glyphs}} looks up "glyphs" on
89
+ # each cell hash automatically -- no need to write {{#each
90
+ # this.glyphs}} or pass an explicit binding. See
91
+ # lib/rhales/core/template_engine.rb EachContext#get for the
92
+ # fallthrough order (current item -> parent context).
93
+ #
94
+ # 5. Cache layer caveat. If a fragment cache wraps the response, the
95
+ # "fresh on every render" guarantee becomes "fresh per cache miss."
96
+ # Worst case is the same tokens until the cache TTL expires, which
97
+ # matches the behavior of a fully static template. For anything
98
+ # where per-request uniqueness actually matters, exclude this block
99
+ # from the cache.
100
+ #
101
+ # Use as a loader:
102
+ #
103
+ # The original motivation for this template was a pre-boot SPA loader
104
+ # that "looks like" something is being computed. To wire that up, wrap
105
+ # the output in role="status" with a visually-hidden label so screen
106
+ # readers announce it correctly, and add CSS that scrolls each cell's
107
+ # glyphs vertically before locking onto a final character. The 17
108
+ # glyphs per cell are sized for a 16-step CSS animation with one
109
+ # resting frame:
110
+ #
111
+ # <div class="tokens" role="status" aria-label="Loading">
112
+ # ... iteration ...
113
+ # <span class="visually-hidden">Loading</span>
114
+ # </div>
115
+ #
116
+ # The animation itself is just CSS keyframes on the iterated spans and
117
+ # is independent of rhales -- the rhales-specific work ends at
118
+ # rendering the markup with fresh random characters per request.
119
+ </logic>
@@ -157,6 +157,9 @@ module Rhales
157
157
  # Hydration mismatch reporting settings
158
158
  attr_accessor :hydration_mismatch_format, :hydration_authority
159
159
 
160
+ # External schema settings
161
+ attr_accessor :schema_search_paths, :schema_tsconfig_path, :schema_use_tsx_import
162
+
160
163
  def initialize
161
164
  # Set sensible defaults
162
165
  @default_locale = 'en'
@@ -191,6 +194,11 @@ module Rhales
191
194
  @hydration_mismatch_format = :compact # :compact, :multiline, :sidebyside, :json
192
195
  @hydration_authority = :schema # :schema or :data
193
196
 
197
+ # External schema defaults
198
+ @schema_search_paths = [] # Additional paths to search for external schemas
199
+ @schema_tsconfig_path = nil # Path to tsconfig.json for tsx import execution
200
+ @schema_use_tsx_import = false # Use tsx import (runs through project tsconfig)
201
+
194
202
  # Yield to block for configuration if provided
195
203
  yield(self) if block_given?
196
204
  end
@@ -257,6 +265,13 @@ module Rhales
257
265
  end
258
266
  end
259
267
 
268
+ # Validate schema search paths exist if specified
269
+ @schema_search_paths.each do |path|
270
+ unless Dir.exist?(path)
271
+ errors << "Schema search path does not exist: #{path}"
272
+ end
273
+ end
274
+
260
275
  # Validate cache TTL
261
276
  if @cache_ttl && @cache_ttl <= 0
262
277
  errors << 'cache_ttl must be positive'
@@ -269,6 +284,7 @@ module Rhales
269
284
  def freeze!
270
285
  @features.freeze
271
286
  @template_paths.freeze
287
+ @schema_search_paths.freeze
272
288
  freeze
273
289
  end
274
290
 
@@ -39,7 +39,7 @@ module Rhales
39
39
  ALL_SECTIONS = KNOWN_SECTIONS.freeze
40
40
 
41
41
  # Known schema section attributes
42
- KNOWN_SCHEMA_ATTRIBUTES = %w[lang version envelope window merge layout extends].freeze
42
+ KNOWN_SCHEMA_ATTRIBUTES = %w[lang version envelope window merge layout extends src].freeze
43
43
 
44
44
  attr_reader :content, :file_path, :grammar, :ast
45
45
 
@@ -151,6 +151,12 @@ module Rhales
151
151
  schema_attributes['extends']
152
152
  end
153
153
 
154
+ # External schema file reference (optional)
155
+ # When present, schema code is loaded from this path instead of inline content
156
+ def schema_src
157
+ schema_attributes['src']
158
+ end
159
+
154
160
  def section?(name)
155
161
  @grammar.sections.key?(name)
156
162
  end
@@ -202,9 +202,11 @@ module Rhales
202
202
  items = get_variable_value(items_var)
203
203
 
204
204
  if items.respond_to?(:each)
205
- items.map.with_index do |item, index|
205
+ items_array = items.to_a
206
+ total = items_array.size
207
+ items_array.map.with_index do |item, index|
206
208
  # Create context for each iteration
207
- item_context = create_each_context(item, index, items_var)
209
+ item_context = create_each_context(item, index, items_var, total)
208
210
  engine = self.class.new('', item_context, partial_resolver: @partial_resolver)
209
211
  engine.send(:render_content_nodes, block_content)
210
212
  end.join
@@ -312,8 +314,8 @@ module Rhales
312
314
  end
313
315
 
314
316
  # Create context for each iteration
315
- def create_each_context(item, index, items_var)
316
- EachContext.new(@context, item, index, items_var)
317
+ def create_each_context(item, index, items_var, total = nil)
318
+ EachContext.new(@context, item, index, items_var, total)
317
319
  end
318
320
 
319
321
  # HTML escape for XSS protection
@@ -323,13 +325,14 @@ module Rhales
323
325
 
324
326
  # Context wrapper for {{#each}} iterations
325
327
  class EachContext
326
- attr_reader :parent_context, :current_item, :current_index, :items_var
328
+ attr_reader :parent_context, :current_item, :current_index, :items_var, :total
327
329
 
328
- def initialize(parent_context, current_item, current_index, items_var)
330
+ def initialize(parent_context, current_item, current_index, items_var, total = nil)
329
331
  @parent_context = parent_context
330
332
  @current_item = current_item
331
333
  @current_index = current_index
332
334
  @items_var = items_var
335
+ @total = total
333
336
  end
334
337
 
335
338
  def get(variable_name)
@@ -342,8 +345,7 @@ module Rhales
342
345
  when '@first'
343
346
  return @current_index == 0
344
347
  when '@last'
345
- # We'd need to know the total length for this
346
- return false
348
+ return @total ? @current_index == @total - 1 : false
347
349
  end
348
350
 
349
351
  # Check if it's a property of the current item
@@ -27,10 +27,6 @@ module Rhales
27
27
  }
28
28
  end
29
29
 
30
- def clear!
31
- Thread.current[:rhales_hydration_registry] = {}
32
- end
33
-
34
30
  # Expose registry for testing purposes
35
31
  def registry
36
32
  thread_local_registry
@@ -343,7 +343,7 @@ module Rhales
343
343
  end
344
344
 
345
345
  # Preprocess content to strip XML/HTML comments outside of sections
346
- # Uses Ruby 3.4+ pattern matching for robust, secure parsing
346
+ # Uses Ruby 3.2+ pattern matching for robust, secure parsing
347
347
  def preprocess_content(content)
348
348
  tokens = tokenize_content(content)
349
349
 
@@ -71,7 +71,18 @@ module Rhales
71
71
  return nil unless doc.section?('schema')
72
72
 
73
73
  template_name = derive_template_name(file_path)
74
- schema_code = doc.section('schema')
74
+ src = doc.schema_src
75
+ resolved_path = nil
76
+ schema_code = nil
77
+
78
+ if src
79
+ # External schema: resolve path and read content
80
+ resolved_path = resolve_schema_src_path(file_path, src)
81
+ schema_code = read_schema_from_src(resolved_path, src, template_name)
82
+ else
83
+ # Inline schema: use content from the schema section
84
+ schema_code = doc.section('schema')
85
+ end
75
86
 
76
87
  {
77
88
  template_name: template_name,
@@ -83,7 +94,9 @@ module Rhales
83
94
  window: doc.schema_window,
84
95
  merge: doc.schema_merge_strategy,
85
96
  layout: doc.schema_layout,
86
- extends: doc.schema_extends
97
+ extends: doc.schema_extends,
98
+ src: src,
99
+ resolved_path: resolved_path
87
100
  }
88
101
  end
89
102
 
@@ -97,15 +110,20 @@ module Rhales
97
110
 
98
111
  # Count how many .rue files have schema sections
99
112
  #
100
- # @return [Hash] Count information
113
+ # @return [Hash] Count information including external vs inline breakdown
101
114
  def schema_stats
102
115
  all_files = find_rue_files
103
116
  schemas = extract_all
104
117
 
118
+ external_count = schemas.count { |s| s[:src] }
119
+ inline_count = schemas.count { |s| s[:src].nil? }
120
+
105
121
  {
106
122
  total_files: all_files.count,
107
123
  files_with_schemas: schemas.count,
108
124
  files_without_schemas: all_files.count - schemas.count,
125
+ external_schemas: external_count,
126
+ inline_schemas: inline_count,
109
127
  schemas_by_lang: schemas.group_by { |s| s[:lang] }.transform_values(&:count)
110
128
  }
111
129
  end
@@ -128,5 +146,105 @@ module Rhales
128
146
  relative_path = file_pathname.relative_path_from(templates_pathname)
129
147
  relative_path.to_s.sub(/\.rue$/, '')
130
148
  end
149
+
150
+ # Resolve external schema src path
151
+ #
152
+ # Resolution order:
153
+ # 1. Relative to template file directory
154
+ # 2. Search through configured schema_search_paths
155
+ #
156
+ # @param template_path [String] Absolute path to the .rue template
157
+ # @param src [String] The src attribute value from the schema tag
158
+ # @return [String] Absolute path to the external schema file
159
+ # @raise [ExtractionError] If path traversal is detected or file not found
160
+ def resolve_schema_src_path(template_path, src)
161
+ template_dir = File.dirname(template_path)
162
+ resolved = File.expand_path(src, template_dir)
163
+ searched_paths = [resolved]
164
+
165
+ # First, check if the path exists relative to template
166
+ if File.exist?(resolved) && path_within_allowed_directories?(resolved)
167
+ return resolved
168
+ end
169
+
170
+ # If the relative path does not exist or is not allowed,
171
+ # search through configured schema_search_paths
172
+ search_paths = Rhales.configuration.schema_search_paths || []
173
+ search_paths.each do |search_path|
174
+ expanded_search_path = File.expand_path(search_path)
175
+ candidate = File.join(expanded_search_path, src)
176
+ searched_paths << candidate
177
+
178
+ if File.exist?(candidate) && path_within_allowed_directories?(candidate)
179
+ return candidate
180
+ end
181
+ end
182
+
183
+ # Security check on the template-relative path
184
+ unless path_within_allowed_directories?(resolved)
185
+ raise ExtractionError,
186
+ "Schema src path traversal not allowed: '#{src}' resolves outside allowed directories"
187
+ end
188
+
189
+ # File not found in any location - raise helpful error listing all searched paths
190
+ raise ExtractionError,
191
+ "Schema file not found: '#{src}'. Searched:\n - #{searched_paths.join("\n - ")}"
192
+ end
193
+
194
+ # Check if a path is within any allowed directory
195
+ #
196
+ # Allowed directories include:
197
+ # - The templates directory
198
+ # - Any configured schema_search_paths
199
+ #
200
+ # @param path [String] Path to check
201
+ # @return [Boolean] True if path is within an allowed directory
202
+ def path_within_allowed_directories?(path)
203
+ return true if path_within_directory?(path, @templates_dir)
204
+
205
+ search_paths = Rhales.configuration.schema_search_paths || []
206
+ search_paths.any? do |search_path|
207
+ expanded_search_path = File.expand_path(search_path)
208
+ path_within_directory?(path, expanded_search_path)
209
+ end
210
+ end
211
+
212
+ # Read schema content from external file
213
+ #
214
+ # @param resolved_path [String] Absolute path to the schema file
215
+ # @param src [String] Original src attribute value (for error messages)
216
+ # @param template_name [String] Template name (for error messages)
217
+ # @return [String] Schema file content
218
+ # @raise [ExtractionError] If file cannot be read
219
+ def read_schema_from_src(resolved_path, src, template_name)
220
+ unless File.exist?(resolved_path)
221
+ raise ExtractionError,
222
+ "External schema file not found: '#{src}' (resolved to: #{resolved_path}) " \
223
+ "referenced by template '#{template_name}'"
224
+ end
225
+
226
+ File.read(resolved_path)
227
+ rescue Errno::EACCES => e
228
+ raise ExtractionError,
229
+ "Permission denied reading external schema '#{src}': #{e.message}"
230
+ rescue Errno::EISDIR
231
+ raise ExtractionError,
232
+ "External schema path '#{src}' is a directory, not a file"
233
+ end
234
+
235
+ # Check if a path is within a given directory (security check)
236
+ #
237
+ # @param path [String] Path to check
238
+ # @param directory [String] Directory that should contain the path
239
+ # @return [Boolean] True if path is within directory
240
+ def path_within_directory?(path, directory)
241
+ expanded_path = File.expand_path(path)
242
+ expanded_dir = File.expand_path(directory)
243
+
244
+ # Ensure directory ends with separator for accurate prefix matching
245
+ expanded_dir_with_sep = expanded_dir.end_with?(File::SEPARATOR) ? expanded_dir : "#{expanded_dir}#{File::SEPARATOR}"
246
+
247
+ expanded_path.start_with?(expanded_dir_with_sep) || expanded_path == expanded_dir
248
+ end
131
249
  end
132
250
  end
@@ -3,6 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'open3'
6
+ require 'securerandom'
6
7
  require 'tempfile'
7
8
  require 'fileutils'
8
9
  require_relative 'schema_extractor'
@@ -76,7 +77,8 @@ module Rhales
76
77
  rescue => e
77
78
  results[:failed] += 1
78
79
  results[:success] = false
79
- error_msg = "Failed to generate schema for #{schema_info[:template_name]}: #{e.message}"
80
+ source_info = schema_info[:src] ? " (from #{schema_info[:src]})" : ""
81
+ error_msg = "Failed to generate schema for #{schema_info[:template_name]}#{source_info}: #{e.message}"
80
82
  results[:errors] << error_msg
81
83
  warn error_msg
82
84
  end
@@ -95,14 +97,20 @@ module Rhales
95
97
  FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
96
98
 
97
99
  temp_file = Tempfile.new(['schema', '.mts'], temp_dir)
100
+ bundled_file = nil
98
101
 
99
102
  begin
100
- # Write TypeScript script
101
- temp_file.write(build_typescript_script(schema_info))
103
+ # Write TypeScript script - use import mode for external schemas when configured
104
+ if use_tsx_import_mode?(schema_info)
105
+ script, bundled_file = build_typescript_import_script(schema_info)
106
+ else
107
+ script = build_typescript_script(schema_info)
108
+ end
109
+ temp_file.write(script)
102
110
  temp_file.close
103
111
 
104
- # Execute with tsx via pnpm
105
- stdout, stderr, status = Open3.capture3('pnpm', 'exec', 'tsx', temp_file.path)
112
+ # Execute with tsx via pnpm, optionally with tsconfig
113
+ stdout, stderr, status = execute_tsx(temp_file.path)
106
114
 
107
115
  unless status.success?
108
116
  raise GenerationError, "TypeScript execution failed: #{stderr}"
@@ -117,17 +125,132 @@ module Rhales
117
125
  json_schema
118
126
  ensure
119
127
  temp_file.unlink if temp_file
128
+ File.unlink(bundled_file) if bundled_file && File.exist?(bundled_file)
120
129
  end
121
130
  end
122
131
 
123
132
  private
124
133
 
134
+ # Determine if we should use tsx import mode for this schema
135
+ #
136
+ # Import mode is used when:
137
+ # 1. schema_use_tsx_import is enabled in configuration
138
+ # 2. The schema has an external src (not inline)
139
+ # 3. The resolved_path exists
140
+ #
141
+ # @param schema_info [Hash] Schema information
142
+ # @return [Boolean]
143
+ def use_tsx_import_mode?(schema_info)
144
+ return false unless Rhales.configuration.schema_use_tsx_import
145
+ return false unless schema_info[:src]
146
+ return false unless schema_info[:resolved_path]
147
+
148
+ File.exist?(schema_info[:resolved_path])
149
+ end
150
+
151
+ # Execute tsx with optional tsconfig
152
+ #
153
+ # @param script_path [String] Path to the TypeScript script to execute
154
+ # @return [Array] stdout, stderr, status from Open3.capture3
155
+ def execute_tsx(script_path)
156
+ tsconfig_path = Rhales.configuration.schema_tsconfig_path
157
+
158
+ if tsconfig_path
159
+ if File.exist?(tsconfig_path)
160
+ Open3.capture3('pnpm', 'exec', 'tsx', '--tsconfig', tsconfig_path, script_path)
161
+ else
162
+ warn "[Rhales] Warning: schema_tsconfig_path '#{tsconfig_path}' does not exist, ignoring"
163
+ Open3.capture3('pnpm', 'exec', 'tsx', script_path)
164
+ end
165
+ else
166
+ Open3.capture3('pnpm', 'exec', 'tsx', script_path)
167
+ end
168
+ end
169
+
170
+ # Build TypeScript script from bundled external schema
171
+ #
172
+ # Uses esbuild to bundle the external schema with all imports resolved,
173
+ # writes to a temp file, then imports via default export. This allows
174
+ # external files to name their schema variable anything they want.
175
+ #
176
+ # External schema files must use default export:
177
+ # const mySchema = z.object({ ... });
178
+ # export default mySchema;
179
+ #
180
+ # @param schema_info [Hash] Schema information with resolved_path
181
+ # @return [Array<String, String>] [script content, bundled_file_path] - caller must clean up bundled file
182
+ def build_typescript_import_script(schema_info)
183
+ safe_name = schema_info[:template_name].gsub("'", "\\'")
184
+ schema_path = schema_info[:resolved_path]
185
+
186
+ # Bundle external schema with esbuild to temp file - resolves all imports
187
+ # Use SecureRandom for uniqueness across concurrent invocations
188
+ temp_dir = File.join(Dir.pwd, 'tmp')
189
+ FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
190
+ unique_suffix = "#{Process.pid}_#{SecureRandom.hex(4)}"
191
+ bundled_file = File.join(temp_dir, "bundled_#{File.basename(schema_path, '.*')}_#{unique_suffix}.mjs")
192
+
193
+ stdout, stderr, status = Open3.capture3(
194
+ 'pnpm', 'exec', 'esbuild', schema_path,
195
+ '--bundle', '--format=esm', '--platform=node',
196
+ '--external:zod',
197
+ "--outfile=#{bundled_file}"
198
+ )
199
+
200
+ unless status.success?
201
+ raise GenerationError, "esbuild bundling failed for #{schema_path}: #{stderr}"
202
+ end
203
+
204
+ # Convert to file:// URL for cross-platform ESM import compatibility
205
+ bundled_file_url = path_to_file_url(bundled_file)
206
+
207
+ script = <<~TYPESCRIPT
208
+ // Auto-generated schema generator for #{safe_name}
209
+ // Source: #{schema_info[:src]} (bundled via esbuild)
210
+ import { z } from 'zod/v4';
211
+ import schema from '#{bundled_file_url}';
212
+
213
+ // Generate JSON Schema
214
+ try {
215
+ const jsonSchema = z.toJSONSchema(schema, {
216
+ target: 'draft-2020-12',
217
+ unrepresentable: 'any',
218
+ cycles: 'ref',
219
+ reused: 'inline',
220
+ });
221
+
222
+ // Add metadata
223
+ const schemaWithMeta = {
224
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
225
+ $id: `https://rhales.dev/schemas/#{safe_name}.json`,
226
+ title: '#{safe_name}',
227
+ description: 'Schema for #{safe_name} template',
228
+ ...jsonSchema,
229
+ };
230
+
231
+ // Output JSON to stdout
232
+ console.log(JSON.stringify(schemaWithMeta, null, 2));
233
+ } catch (error) {
234
+ console.error('Schema generation error:', error.message);
235
+ process.exit(1);
236
+ }
237
+ TYPESCRIPT
238
+
239
+ [script, bundled_file]
240
+ end
241
+
125
242
  def build_typescript_script(schema_info)
126
243
  # Escape single quotes in template name for TypeScript string
127
244
  safe_name = schema_info[:template_name].gsub("'", "\\'")
245
+ source_comment = if schema_info[:src]
246
+ "// Source: #{schema_info[:src]} (external)"
247
+ else
248
+ "// Source: inline schema"
249
+ end
128
250
 
129
251
  <<~TYPESCRIPT
130
252
  // Auto-generated schema generator for #{safe_name}
253
+ #{source_comment}
131
254
  import { z } from 'zod/v4';
132
255
 
133
256
  // Schema code from .rue template
@@ -185,10 +308,34 @@ module Rhales
185
308
  unless status.success?
186
309
  raise GenerationError, "tsx not found. Run: pnpm install tsx --save-dev"
187
310
  end
311
+
312
+ # Check esbuild is available when tsx import mode is enabled
313
+ if Rhales.configuration.schema_use_tsx_import
314
+ stdout, stderr, status = Open3.capture3('pnpm', 'exec', 'esbuild', '--version')
315
+ unless status.success?
316
+ raise GenerationError, "esbuild not found (required for external schema bundling). Run: pnpm install esbuild --save-dev"
317
+ end
318
+ end
188
319
  end
189
320
 
190
321
  def ensure_output_directory!
191
322
  FileUtils.mkdir_p(@output_dir) unless File.directory?(@output_dir)
192
323
  end
324
+
325
+ # Convert absolute path to file:// URL for cross-platform ESM imports
326
+ #
327
+ # On Windows, paths like C:\foo\bar need to become file:///C:/foo/bar
328
+ # On Unix, paths like /foo/bar become file:///foo/bar
329
+ #
330
+ # @param path [String] Absolute file path
331
+ # @return [String] file:// URL
332
+ def path_to_file_url(path)
333
+ normalized = path.tr('\\', '/')
334
+ if normalized.match?(%r{^[A-Za-z]:}) # Windows drive letter
335
+ "file:///#{normalized}"
336
+ else
337
+ "file://#{normalized}"
338
+ end
339
+ end
193
340
  end
194
341
  end
@@ -5,6 +5,6 @@
5
5
  module Rhales
6
6
  # Version information for the RSFC gem
7
7
  unless defined?(Rhales::VERSION)
8
- VERSION = '0.5.4'
8
+ VERSION = '0.6.2'
9
9
  end
10
10
  end
@@ -45,7 +45,8 @@ namespace :rhales do
45
45
 
46
46
  puts "Found #{schemas.size} schema section(s):"
47
47
  schemas.each do |schema|
48
- puts " - #{schema[:template_name]} (#{schema[:lang]})"
48
+ source_type = schema[:src] ? "external: #{schema[:src]}" : "inline"
49
+ puts " - #{schema[:template_name]} (#{schema[:lang]}, #{source_type})"
49
50
  end
50
51
  puts
51
52
 
@@ -186,6 +187,13 @@ namespace :rhales do
186
187
  puts "Files without <schema>: #{stats[:files_without_schemas]}"
187
188
  puts
188
189
 
190
+ if stats[:files_with_schemas] > 0
191
+ puts "Schema sources:"
192
+ puts " External (src attribute): #{stats[:external_schemas]}"
193
+ puts " Inline: #{stats[:inline_schemas]}"
194
+ puts
195
+ end
196
+
189
197
  if stats[:schemas_by_lang].any?
190
198
  puts "By language:"
191
199
  stats[:schemas_by_lang].each do |lang, count|
data/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "dependencies": {
4
- "zod": "^4.1.12"
4
+ "zod": "^4.3.6"
5
5
  },
6
6
  "devDependencies": {
7
7
  "@prettier/plugin-ruby": "^4.0.4",
data/pnpm-lock.yaml CHANGED
@@ -9,8 +9,8 @@ importers:
9
9
  .:
10
10
  dependencies:
11
11
  zod:
12
- specifier: ^4.1.12
13
- version: 4.1.12
12
+ specifier: ^4.3.6
13
+ version: 4.3.6
14
14
  devDependencies:
15
15
  '@prettier/plugin-ruby':
16
16
  specifier: ^4.0.4
@@ -208,8 +208,8 @@ packages:
208
208
  engines: {node: '>=18.0.0'}
209
209
  hasBin: true
210
210
 
211
- zod@4.1.12:
212
- resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
211
+ zod@4.3.6:
212
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
213
213
 
214
214
  snapshots:
215
215
 
@@ -342,4 +342,4 @@ snapshots:
342
342
  optionalDependencies:
343
343
  fsevents: 2.3.3
344
344
 
345
- zod@4.1.12: {}
345
+ zod@4.3.6: {}
data/rhales.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.homepage = 'https://github.com/onetimesecret/rhales'
23
23
  spec.license = 'MIT'
24
- spec.required_ruby_version = '>= 3.3.4'
24
+ spec.required_ruby_version = '>= 3.2'
25
25
 
26
26
  spec.metadata['source_code_uri'] = 'https://github.com/onetimesecret/rhales'
27
27
  spec.metadata['changelog_uri'] = 'https://github.com/onetimesecret/rhales/blob/main/CHANGELOG.md'
@@ -41,14 +41,11 @@ Gem::Specification.new do |spec|
41
41
  spec.require_paths = ['lib']
42
42
 
43
43
  # Runtime dependencies
44
- spec.add_dependency 'json_schemer', '~> 2.3' # JSON Schema validation in middleware
44
+
45
+ spec.add_dependency 'json_schemer', '~> 2' # JSON Schema validation in middleware
45
46
  spec.add_dependency 'logger' # Standard library logger for logging support
46
47
  spec.add_dependency 'tilt', '~> 2' # Templating engine for rendering RSFCs
47
48
 
48
- # Optional dependencies for performance optimization
49
- # Install oj for 10-20x faster JSON parsing and 5-10x faster generation
50
- # spec.add_dependency 'oj', '~> 3.13'
51
-
52
49
  # Development dependencies should be specified in Gemfile instead of gemspec
53
50
  # See: https://bundler.io/guides/creating_gem.html#testing-our-gem
54
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhales
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - delano
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '2.3'
18
+ version: '2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '2.3'
25
+ version: '2'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: logger
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,7 @@ files:
70
70
  - ".github/workflows/claude-code-review.yml"
71
71
  - ".github/workflows/claude.yml"
72
72
  - ".github/workflows/code-smells.yml"
73
+ - ".github/workflows/release-gem.yml"
73
74
  - ".github/workflows/ruby-lint.yml"
74
75
  - ".github/workflows/yardoc.yml"
75
76
  - ".gitignore"
@@ -120,6 +121,7 @@ files:
120
121
  - examples/dashboard-with-charts.rue
121
122
  - examples/form-with-validation.rue
122
123
  - examples/simple-page.rue
124
+ - examples/token-loader.rue
123
125
  - examples/vue.rue
124
126
  - generate-json-schemas.ts
125
127
  - json_schemer_migration_summary.md
@@ -189,14 +191,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
189
191
  requirements:
190
192
  - - ">="
191
193
  - !ruby/object:Gem::Version
192
- version: 3.3.4
194
+ version: '3.2'
193
195
  required_rubygems_version: !ruby/object:Gem::Requirement
194
196
  requirements:
195
197
  - - ">="
196
198
  - !ruby/object:Gem::Version
197
199
  version: '0'
198
200
  requirements: []
199
- rubygems_version: 3.7.2
201
+ rubygems_version: 4.0.10
200
202
  specification_version: 4
201
203
  summary: Rhales - Server-rendered components with client-side hydration (RSFCs)
202
204
  test_files: []