rhales 0.4.0 → 0.5.3

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/renovate.json5 +52 -0
  3. data/.github/workflows/ci.yml +123 -0
  4. data/.github/workflows/claude-code-review.yml +69 -0
  5. data/.github/workflows/claude.yml +49 -0
  6. data/.github/workflows/code-smells.yml +146 -0
  7. data/.github/workflows/ruby-lint.yml +78 -0
  8. data/.github/workflows/yardoc.yml +126 -0
  9. data/.gitignore +55 -0
  10. data/.pr_agent.toml +63 -0
  11. data/.pre-commit-config.yaml +89 -0
  12. data/.prettierignore +8 -0
  13. data/.prettierrc +38 -0
  14. data/.reek.yml +98 -0
  15. data/.rubocop.yml +428 -0
  16. data/.serena/.gitignore +3 -0
  17. data/.yardopts +56 -0
  18. data/CHANGELOG.md +44 -0
  19. data/CLAUDE.md +1 -1
  20. data/Gemfile +29 -0
  21. data/Gemfile.lock +189 -0
  22. data/README.md +686 -868
  23. data/Rakefile +46 -0
  24. data/debug_context.rb +25 -0
  25. data/demo/rhales-roda-demo/.gitignore +7 -0
  26. data/demo/rhales-roda-demo/Gemfile +32 -0
  27. data/demo/rhales-roda-demo/Gemfile.lock +151 -0
  28. data/demo/rhales-roda-demo/MAIL.md +405 -0
  29. data/demo/rhales-roda-demo/README.md +376 -0
  30. data/demo/rhales-roda-demo/RODA-TEMPLATE-ENGINES.md +192 -0
  31. data/demo/rhales-roda-demo/Rakefile +49 -0
  32. data/demo/rhales-roda-demo/app.rb +325 -0
  33. data/demo/rhales-roda-demo/bin/rackup +26 -0
  34. data/demo/rhales-roda-demo/config.ru +13 -0
  35. data/demo/rhales-roda-demo/db/migrate/001_create_rodauth_tables.rb +266 -0
  36. data/demo/rhales-roda-demo/db/migrate/002_create_rodauth_password_tables.rb +79 -0
  37. data/demo/rhales-roda-demo/db/migrate/003_add_admin_account.rb +68 -0
  38. data/demo/rhales-roda-demo/templates/change_login.rue +31 -0
  39. data/demo/rhales-roda-demo/templates/change_password.rue +36 -0
  40. data/demo/rhales-roda-demo/templates/close_account.rue +31 -0
  41. data/demo/rhales-roda-demo/templates/create_account.rue +40 -0
  42. data/demo/rhales-roda-demo/templates/dashboard.rue +150 -0
  43. data/demo/rhales-roda-demo/templates/home.rue +78 -0
  44. data/demo/rhales-roda-demo/templates/layouts/main.rue +168 -0
  45. data/demo/rhales-roda-demo/templates/login.rue +65 -0
  46. data/demo/rhales-roda-demo/templates/logout.rue +25 -0
  47. data/demo/rhales-roda-demo/templates/reset_password.rue +26 -0
  48. data/demo/rhales-roda-demo/templates/verify_account.rue +27 -0
  49. data/demo/rhales-roda-demo/test_full_output.rb +27 -0
  50. data/demo/rhales-roda-demo/test_simple.rb +24 -0
  51. data/docs/.gitignore +9 -0
  52. data/docs/architecture/data-flow.md +499 -0
  53. data/examples/dashboard-with-charts.rue +271 -0
  54. data/examples/form-with-validation.rue +180 -0
  55. data/examples/simple-page.rue +61 -0
  56. data/examples/vue.rue +136 -0
  57. data/generate-json-schemas.ts +158 -0
  58. data/json_schemer_migration_summary.md +172 -0
  59. data/lib/rhales/adapters/base_auth.rb +2 -0
  60. data/lib/rhales/adapters/base_request.rb +2 -0
  61. data/lib/rhales/adapters/base_session.rb +2 -0
  62. data/lib/rhales/adapters.rb +7 -0
  63. data/lib/rhales/configuration.rb +47 -0
  64. data/lib/rhales/core/context.rb +354 -0
  65. data/lib/rhales/{rue_document.rb → core/rue_document.rb} +56 -38
  66. data/lib/rhales/{template_engine.rb → core/template_engine.rb} +66 -59
  67. data/lib/rhales/{view.rb → core/view.rb} +112 -135
  68. data/lib/rhales/{view_composition.rb → core/view_composition.rb} +78 -8
  69. data/lib/rhales/core.rb +9 -0
  70. data/lib/rhales/errors/hydration_collision_error.rb +2 -0
  71. data/lib/rhales/errors.rb +2 -0
  72. data/lib/rhales/{earliest_injection_detector.rb → hydration/earliest_injection_detector.rb} +4 -0
  73. data/lib/rhales/hydration/hydration_data_aggregator.rb +487 -0
  74. data/lib/rhales/{hydration_endpoint.rb → hydration/hydration_endpoint.rb} +16 -12
  75. data/lib/rhales/{hydration_injector.rb → hydration/hydration_injector.rb} +4 -0
  76. data/lib/rhales/{hydration_registry.rb → hydration/hydration_registry.rb} +2 -0
  77. data/lib/rhales/hydration/hydrator.rb +102 -0
  78. data/lib/rhales/{link_based_injection_detector.rb → hydration/link_based_injection_detector.rb} +4 -0
  79. data/lib/rhales/{mount_point_detector.rb → hydration/mount_point_detector.rb} +4 -0
  80. data/lib/rhales/{safe_injection_validator.rb → hydration/safe_injection_validator.rb} +4 -0
  81. data/lib/rhales/hydration.rb +13 -0
  82. data/lib/rhales/{refinements → integrations/refinements}/require_refinements.rb +3 -1
  83. data/lib/rhales/{tilt.rb → integrations/tilt.rb} +22 -15
  84. data/lib/rhales/integrations.rb +6 -0
  85. data/lib/rhales/middleware/json_responder.rb +191 -0
  86. data/lib/rhales/middleware/schema_validator.rb +300 -0
  87. data/lib/rhales/middleware.rb +6 -0
  88. data/lib/rhales/parsers/handlebars_parser.rb +2 -0
  89. data/lib/rhales/parsers/rue_format_parser.rb +9 -7
  90. data/lib/rhales/parsers.rb +9 -0
  91. data/lib/rhales/{csp.rb → security/csp.rb} +27 -3
  92. data/lib/rhales/utils/json_serializer.rb +114 -0
  93. data/lib/rhales/utils/logging_helpers.rb +75 -0
  94. data/lib/rhales/utils/schema_extractor.rb +132 -0
  95. data/lib/rhales/utils/schema_generator.rb +194 -0
  96. data/lib/rhales/utils.rb +40 -0
  97. data/lib/rhales/version.rb +3 -1
  98. data/lib/rhales.rb +41 -24
  99. data/lib/tasks/rhales_schema.rake +197 -0
  100. data/package.json +10 -0
  101. data/pnpm-lock.yaml +345 -0
  102. data/pnpm-workspace.yaml +2 -0
  103. data/proofs/error_handling.rb +79 -0
  104. data/proofs/expanded_object_inheritance.rb +82 -0
  105. data/proofs/partial_context_scoping_fix.rb +168 -0
  106. data/proofs/ui_context_partial_inheritance.rb +236 -0
  107. data/rhales.gemspec +14 -6
  108. data/schema_vs_data_comparison.md +254 -0
  109. data/test_direct_access.rb +36 -0
  110. metadata +141 -23
  111. data/CLAUDE.locale.txt +0 -7
  112. data/lib/rhales/context.rb +0 -239
  113. data/lib/rhales/hydration_data_aggregator.rb +0 -221
  114. data/lib/rhales/hydrator.rb +0 -141
  115. data/lib/rhales/parsers/handlebars-grammar-review.txt +0 -39
@@ -0,0 +1,271 @@
1
+ <!-- examples/dashboard-with-charts.rue -->
2
+
3
+ <!--
4
+ Example: Dashboard with Complex Data Hydration
5
+
6
+ This demonstrates:
7
+ - Complex nested data structures with Zod v4 schema
8
+ - Arrays and objects for chart data
9
+ - Conditional rendering based on data availability
10
+ - Integration with client-side charting libraries
11
+ - Proper separation of client vs server data
12
+ -->
13
+
14
+ <schema lang="js-zod" window="dashboardData">
15
+ const schema = z.object({
16
+ user: z.object({
17
+ id: z.number(),
18
+ name: z.string(),
19
+ role: z.enum(['admin', 'user', 'guest'])
20
+ }),
21
+ stats: z.object({
22
+ totalUsers: z.number(),
23
+ activeUsers: z.number(),
24
+ revenue: z.number(),
25
+ growth: z.number()
26
+ }),
27
+ chartData: z.object({
28
+ labels: z.array(z.string()),
29
+ datasets: z.array(z.object({
30
+ label: z.string(),
31
+ data: z.array(z.number()),
32
+ backgroundColor: z.string().optional(),
33
+ borderColor: z.string().optional()
34
+ }))
35
+ }),
36
+ recentActivity: z.array(z.object({
37
+ id: z.number(),
38
+ user: z.string(),
39
+ action: z.string(),
40
+ timestamp: z.number()
41
+ })),
42
+ permissions: z.object({
43
+ canEdit: z.boolean(),
44
+ canDelete: z.boolean(),
45
+ canExport: z.boolean()
46
+ })
47
+ });
48
+ </schema>
49
+
50
+ <template>
51
+ <!DOCTYPE html>
52
+ <html lang="{{request.locale}}">
53
+ <head>
54
+ <meta charset="utf-8">
55
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
56
+ <title>{{server.pageTitle}}</title>
57
+ <style>
58
+ body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
59
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
60
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
61
+ .stat-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
62
+ .stat-value { font-size: 2rem; font-weight: bold; color: #0066cc; }
63
+ .stat-label { color: #666; margin-top: 0.5rem; }
64
+ .chart-container { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
65
+ .activity-list { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
66
+ .activity-item { padding: 1rem; border-bottom: 1px solid #eee; }
67
+ .activity-item:last-child { border-bottom: none; }
68
+ .actions { display: flex; gap: 0.5rem; }
69
+ .btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; }
70
+ .btn-primary { background: #0066cc; color: white; }
71
+ .btn-danger { background: #dc3545; color: white; }
72
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
73
+ </style>
74
+ <!-- Chart.js for visualization -->
75
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
76
+ </head>
77
+ <body>
78
+ <div class="header">
79
+ <div>
80
+ <h1>{{server.pageTitle}}</h1>
81
+ <p>Welcome back, {{client.user.name}} ({{client.user.role}})</p>
82
+ </div>
83
+ <div class="actions">
84
+ {{#if client.permissions.canExport}}
85
+ <button class="btn btn-primary" onclick="exportData()">Export Data</button>
86
+ {{/if}}
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Stats Overview -->
91
+ <div class="stats-grid">
92
+ <div class="stat-card">
93
+ <div class="stat-value">{{client.stats.totalUsers}}</div>
94
+ <div class="stat-label">Total Users</div>
95
+ </div>
96
+ <div class="stat-card">
97
+ <div class="stat-value">{{client.stats.activeUsers}}</div>
98
+ <div class="stat-label">Active Users</div>
99
+ </div>
100
+ <div class="stat-card">
101
+ <div class="stat-value">${{client.stats.revenue}}</div>
102
+ <div class="stat-label">Revenue</div>
103
+ </div>
104
+ <div class="stat-card">
105
+ <div class="stat-value">{{client.stats.growth}}%</div>
106
+ <div class="stat-label">Growth</div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Chart -->
111
+ <div class="chart-container">
112
+ <h2>Activity Over Time</h2>
113
+ <canvas id="activityChart"></canvas>
114
+ </div>
115
+
116
+ <!-- Recent Activity -->
117
+ <div class="activity-list">
118
+ <h2>Recent Activity</h2>
119
+ {{#if client.recentActivity}}
120
+ {{#each client.recentActivity}}
121
+ <div class="activity-item">
122
+ <strong>{{user}}</strong> {{action}}
123
+ <span style="color: #666; font-size: 0.875rem;">
124
+ ({{@index}} items ago)
125
+ </span>
126
+ {{#if ../client.permissions.canDelete}}
127
+ <button class="btn btn-danger" onclick="deleteActivity({{id}})">Delete</button>
128
+ {{/if}}
129
+ </div>
130
+ {{/each}}
131
+ {{else}}
132
+ <p>No recent activity</p>
133
+ {{/if}}
134
+ </div>
135
+
136
+ <!-- Client-side JavaScript -->
137
+ <script nonce="{{request.nonce}}">
138
+ // Access hydrated dashboard data
139
+ const data = window.dashboardData;
140
+
141
+ // Initialize chart with hydrated data
142
+ const ctx = document.getElementById('activityChart').getContext('2d');
143
+ const chart = new Chart(ctx, {
144
+ type: 'line',
145
+ data: {
146
+ labels: data.chartData.labels,
147
+ datasets: data.chartData.datasets
148
+ },
149
+ options: {
150
+ responsive: true,
151
+ plugins: {
152
+ legend: { position: 'top' }
153
+ },
154
+ scales: {
155
+ y: { beginAtZero: true }
156
+ }
157
+ }
158
+ });
159
+
160
+ // Export function (only available if user has permission)
161
+ function exportData() {
162
+ const csv = generateCSV(data.recentActivity);
163
+ downloadCSV(csv, 'dashboard-export.csv');
164
+ console.log('Data exported by user:', data.user.name);
165
+ }
166
+
167
+ function generateCSV(activities) {
168
+ const headers = ['ID', 'User', 'Action', 'Timestamp'];
169
+ const rows = activities.map(a => [a.id, a.user, a.action, a.timestamp]);
170
+ return [headers, ...rows].map(row => row.join(',')).join('\n');
171
+ }
172
+
173
+ function downloadCSV(csv, filename) {
174
+ const blob = new Blob([csv], { type: 'text/csv' });
175
+ const url = URL.createObjectURL(blob);
176
+ const a = document.createElement('a');
177
+ a.href = url;
178
+ a.download = filename;
179
+ a.click();
180
+ URL.revokeObjectURL(url);
181
+ }
182
+
183
+ // Delete function (only available if user has permission)
184
+ function deleteActivity(id) {
185
+ if (!data.permissions.canDelete) {
186
+ alert('You do not have permission to delete activities');
187
+ return;
188
+ }
189
+
190
+ if (confirm('Are you sure you want to delete this activity?')) {
191
+ fetch(`/api/activities/${id}`, {
192
+ method: 'DELETE',
193
+ headers: {
194
+ 'Content-Type': 'application/json',
195
+ 'X-CSRF-Token': '{{request.csrf_token}}'
196
+ }
197
+ })
198
+ .then(r => r.json())
199
+ .then(result => {
200
+ console.log('Activity deleted:', id);
201
+ location.reload();
202
+ })
203
+ .catch(err => console.error('Delete failed:', err));
204
+ }
205
+ }
206
+ </script>
207
+ </body>
208
+ </html>
209
+ </template>
210
+
211
+ <logic>
212
+ # Dashboard with Charts Example
213
+ #
214
+ # Backend Usage (Ruby):
215
+ #
216
+ # view = Rhales::View.new(
217
+ # request,
218
+ # client: {
219
+ # user: {
220
+ # id: current_user.id,
221
+ # name: current_user.name,
222
+ # role: current_user.role
223
+ # },
224
+ # stats: {
225
+ # totalUsers: User.count,
226
+ # activeUsers: User.active.count,
227
+ # revenue: Order.total_revenue,
228
+ # growth: Analytics.growth_percentage
229
+ # },
230
+ # chartData: {
231
+ # labels: last_7_days.map(&:to_s),
232
+ # datasets: [
233
+ # {
234
+ # label: 'Active Users',
235
+ # data: last_7_days.map { |date| User.active_on(date).count },
236
+ # backgroundColor: 'rgba(54, 162, 235, 0.2)',
237
+ # borderColor: 'rgba(54, 162, 235, 1)'
238
+ # }
239
+ # ]
240
+ # },
241
+ # recentActivity: Activity.recent(10).map { |a|
242
+ # {
243
+ # id: a.id,
244
+ # user: a.user.name,
245
+ # action: a.action_description,
246
+ # timestamp: a.created_at.to_i
247
+ # }
248
+ # },
249
+ # permissions: {
250
+ # canEdit: current_user.can?(:edit),
251
+ # canDelete: current_user.can?(:delete),
252
+ # canExport: current_user.can?(:export)
253
+ # }
254
+ # },
255
+ # server: {
256
+ # pageTitle: 'Admin Dashboard',
257
+ # # Server-only data (not sent to client)
258
+ # internalNotes: 'Premium customer - handle with care',
259
+ # auditLog: AuditLog.for_user(current_user)
260
+ # }
261
+ # )
262
+ #
263
+ # html = view.render('dashboard-with-charts')
264
+ #
265
+ # Key Features:
266
+ # - Complex nested data validated with Zod schema
267
+ # - Permission-based UI rendering (buttons only shown if allowed)
268
+ # - Client-side chart initialization with hydrated data
269
+ # - CSRF protection for DELETE requests
270
+ # - Clear separation: sensitive audit logs stay in server layer
271
+ </logic>
@@ -0,0 +1,180 @@
1
+ <!-- examples/form-with-validation.rue -->
2
+
3
+ <!--
4
+ Example: Form with CSRF Protection and Client-Side Validation
5
+
6
+ This demonstrates:
7
+ - CSRF token handling for secure form submissions
8
+ - Schema validation for form configuration
9
+ - Conditional rendering based on authentication
10
+ - Client-side validation with hydrated data
11
+ -->
12
+
13
+ <schema lang="js-zod" window="formData">
14
+ const schema = z.object({
15
+ formConfig: z.object({
16
+ action: z.string().url(),
17
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
18
+ maxLength: z.number(),
19
+ required: z.array(z.string())
20
+ }),
21
+ user: z.object({
22
+ name: z.string(),
23
+ email: z.string().email()
24
+ }).nullable(),
25
+ validationMessages: z.object({
26
+ required: z.string(),
27
+ email: z.string(),
28
+ maxLength: z.string()
29
+ })
30
+ });
31
+ </schema>
32
+
33
+ <template>
34
+ <!DOCTYPE html>
35
+ <html lang="{{request.locale}}">
36
+ <head>
37
+ <meta charset="utf-8">
38
+ <title>{{server.pageTitle}}</title>
39
+ <style>
40
+ .form-group { margin-bottom: 1rem; }
41
+ .error { color: red; font-size: 0.875rem; }
42
+ label { display: block; margin-bottom: 0.25rem; font-weight: bold; }
43
+ input, textarea { width: 100%; padding: 0.5rem; }
44
+ button { padding: 0.5rem 1rem; background: #0066cc; color: white; border: none; cursor: pointer; }
45
+ button:hover { background: #0052a3; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <h1>{{server.pageTitle}}</h1>
50
+
51
+ {{#if request.authenticated?}}
52
+ <form id="contact-form" action="{{client.formConfig.action}}" method="{{client.formConfig.method}}">
53
+ <!-- CSRF protection -->
54
+ <input type="hidden" name="_csrf" value="{{request.csrf_token}}">
55
+
56
+ <div class="form-group">
57
+ <label for="name">Name *</label>
58
+ <input
59
+ type="text"
60
+ id="name"
61
+ name="name"
62
+ value="{{client.user.name}}"
63
+ required
64
+ maxlength="{{client.formConfig.maxLength}}">
65
+ <span class="error" id="name-error"></span>
66
+ </div>
67
+
68
+ <div class="form-group">
69
+ <label for="email">Email *</label>
70
+ <input
71
+ type="email"
72
+ id="email"
73
+ name="email"
74
+ value="{{client.user.email}}"
75
+ required>
76
+ <span class="error" id="email-error"></span>
77
+ </div>
78
+
79
+ <div class="form-group">
80
+ <label for="message">Message *</label>
81
+ <textarea
82
+ id="message"
83
+ name="message"
84
+ rows="5"
85
+ required
86
+ maxlength="{{client.formConfig.maxLength}}"></textarea>
87
+ <span class="error" id="message-error"></span>
88
+ </div>
89
+
90
+ <button type="submit">Send Message</button>
91
+ </form>
92
+
93
+ <!-- Client-side validation using Zod -->
94
+ <script nonce="{{request.nonce}}">
95
+ const form = document.getElementById('contact-form');
96
+ const config = window.formData.formConfig;
97
+ const messages = window.formData.validationMessages;
98
+
99
+ // Define Zod schema for form validation
100
+ const formSchema = z.object({
101
+ name: z.string()
102
+ .min(1, messages.required)
103
+ .max(config.maxLength, messages.maxLength.replace('{max}', config.maxLength)),
104
+ email: z.string()
105
+ .min(1, messages.required)
106
+ .email(messages.email),
107
+ message: z.string()
108
+ .min(1, messages.required)
109
+ .max(config.maxLength, messages.maxLength.replace('{max}', config.maxLength))
110
+ });
111
+
112
+ form.addEventListener('submit', (e) => {
113
+ // Clear previous errors
114
+ document.querySelectorAll('.error').forEach(el => el.textContent = '');
115
+
116
+ // Collect form data
117
+ const formData = {
118
+ name: form.elements['name'].value,
119
+ email: form.elements['email'].value,
120
+ message: form.elements['message'].value
121
+ };
122
+
123
+ // Validate using Zod
124
+ const result = formSchema.safeParse(formData);
125
+
126
+ if (!result.success) {
127
+ e.preventDefault();
128
+
129
+ // Display validation errors
130
+ result.error.issues.forEach(issue => {
131
+ const fieldName = issue.path[0];
132
+ const errorElement = document.getElementById(fieldName + '-error');
133
+ if (errorElement) {
134
+ errorElement.textContent = issue.message;
135
+ }
136
+ });
137
+ }
138
+ });
139
+ </script>
140
+ {{else}}
141
+ <p>Please <a href="/login">log in</a> to submit the form.</p>
142
+ {{/if}}
143
+ </body>
144
+ </html>
145
+ </template>
146
+
147
+ <logic>
148
+ # Form with Validation Example
149
+ #
150
+ # Backend Usage (Ruby):
151
+ #
152
+ # view = Rhales::View.new(
153
+ # request,
154
+ # client: {
155
+ # formConfig: {
156
+ # action: '/api/contact',
157
+ # method: 'POST',
158
+ # maxLength: 500,
159
+ # required: ['name', 'email', 'message']
160
+ # },
161
+ # user: current_user ? { name: current_user.name, email: current_user.email } : nil,
162
+ # validationMessages: {
163
+ # required: 'This field is required',
164
+ # email: 'Please enter a valid email address',
165
+ # maxLength: 'Maximum length is {max} characters'
166
+ # }
167
+ # },
168
+ # server: {
169
+ # pageTitle: 'Contact Us'
170
+ # }
171
+ # )
172
+ #
173
+ # html = view.render('form-with-validation')
174
+ #
175
+ # Key Security Features:
176
+ # - CSRF token automatically provided via request.csrf_token
177
+ # - CSP nonce for inline scripts via request.nonce
178
+ # - Schema validation ensures form config matches expected structure
179
+ # - Only safe, public user data (name, email) is sent to client
180
+ </logic>
@@ -0,0 +1,61 @@
1
+ <!-- examples/simple-page.rue -->
2
+
3
+ <!--
4
+ Example: Simple Page - Minimal Rhales Example
5
+
6
+ This is the simplest possible Rhales template demonstrating:
7
+ - Basic schema definition with Zod v4
8
+ - Simple template with variable interpolation
9
+ - Three-layer context access (request, server, client)
10
+
11
+ Perfect for beginners learning Rhales fundamentals.
12
+ -->
13
+
14
+ <schema lang="js-zod" window="pageData">
15
+ const schema = z.object({
16
+ message: z.string(),
17
+ count: z.number()
18
+ });
19
+ </schema>
20
+
21
+ <template>
22
+ <!DOCTYPE html>
23
+ <html lang="{{request.locale}}">
24
+ <head>
25
+ <meta charset="utf-8">
26
+ <title>{{server.pageTitle}}</title>
27
+ </head>
28
+ <body>
29
+ <h1>{{client.message}}</h1>
30
+ <p>Count: {{client.count}}</p>
31
+
32
+ <!-- Access hydrated data in client-side JavaScript -->
33
+ <script nonce="{{request.nonce}}">
34
+ console.log('Message:', window.pageData.message);
35
+ console.log('Count:', window.pageData.count);
36
+ </script>
37
+ </body>
38
+ </html>
39
+ </template>
40
+
41
+ <logic>
42
+ # Simple Page Example
43
+ #
44
+ # Backend Usage (Ruby):
45
+ #
46
+ # view = Rhales::View.new(
47
+ # request,
48
+ # client: {
49
+ # message: 'Hello, Rhales!',
50
+ # count: 42
51
+ # },
52
+ # server: {
53
+ # pageTitle: 'Simple Page Example'
54
+ # }
55
+ # )
56
+ #
57
+ # html = view.render('simple-page')
58
+ #
59
+ # The client data is automatically validated against the schema
60
+ # and serialized to window.pageData for client-side JavaScript access.
61
+ </logic>
data/examples/vue.rue ADDED
@@ -0,0 +1,136 @@
1
+ <!-- examples/vue.rue -->
2
+
3
+ <!--
4
+ Example: Vue.js SPA Layout with Server-Side Rendering + Client Hydration
5
+
6
+ This demonstrates Rhales' core strength: seamless server-to-SPA handoff.
7
+
8
+ The <schema> section defines a Zod v4 schema in plain JavaScript that specifies
9
+ exactly what your Vue app receives. The window variable name is configurable via
10
+ the window="..." attribute (e.g., window="appState" → window.appState).
11
+
12
+ The <template> provides SEO-friendly server-rendered HTML that Vue can mount onto
13
+ for client-side functionality.
14
+
15
+ Key features demonstrated:
16
+ - Schema validation with Zod v4
17
+ - Automatic JSON hydration script generation
18
+ - Conditional asset loading (dev vs production)
19
+ - CSP-compliant nonce handling
20
+ - Three-layer context access (request, server, client)
21
+ -->
22
+
23
+ <schema lang="js-zod" window="__ONETIME_STATE__">
24
+ const schema = z.object({
25
+ ui: z.object({
26
+ theme: z.string(),
27
+ locale: z.string()
28
+ }),
29
+ authentication: z.object({
30
+ authenticated: z.boolean(),
31
+ userId: z.string().nullable()
32
+ }),
33
+ user: z.object({
34
+ name: z.string(),
35
+ email: z.string(),
36
+ accountSince: z.number().nullable()
37
+ }).nullable(),
38
+ features: z.object({
39
+ darkMode: z.boolean(),
40
+ notifications: z.boolean()
41
+ }),
42
+ apiBaseUrl: z.string().url()
43
+ });
44
+ </schema>
45
+
46
+ <template>
47
+ <!doctype html>
48
+ <html lang="{{client.ui.locale}}" class="{{client.ui.theme}}">
49
+ <head>
50
+ <meta charset="utf-8">
51
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
+ <title>{{server.pageTitle}}</title>
53
+
54
+ <!-- Dark mode detection (runs before page render to prevent FOUC) -->
55
+ <script nonce="{{request.nonce}}">
56
+ if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
57
+ document.documentElement.classList.add('dark')
58
+ } else {
59
+ document.documentElement.classList.remove('dark')
60
+ }
61
+ </script>
62
+
63
+ {{#if server.viteAssetsHtml}}
64
+ {{{server.viteAssetsHtml}}}
65
+ {{/if}}
66
+ </head>
67
+
68
+ <body class="font-serif bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
69
+ <div id="app">
70
+ <!-- Server-rendered placeholder for SEO/initial load -->
71
+ {{#if client.authentication.authenticated}}
72
+ <div class="loading-state">
73
+ <p>Welcome back, {{client.user.name}}!</p>
74
+ <p>Loading your dashboard...</p>
75
+ </div>
76
+ {{else}}
77
+ <div class="landing-page">
78
+ <h1>Welcome to Our App</h1>
79
+ <p>Please log in to continue</p>
80
+ </div>
81
+ {{/if}}
82
+ </div>
83
+
84
+ <!-- Client hydration data is automatically injected by Rhales hydrator -->
85
+ <!-- Result: window.__ONETIME_STATE__ = { ui: {...}, authentication: {...}, ... } -->
86
+
87
+ <!-- Vue application bootstrap -->
88
+ {{#if server.frontendHost}}
89
+ <!-- Development: Vite dev server -->
90
+ <script nonce="{{request.nonce}}" type="module" src="{{server.frontendHost}}/src/main.ts"></script>
91
+ {{else}}
92
+ <!-- Production: Built assets -->
93
+ <script nonce="{{request.nonce}}" type="module" src="/assets/main.js"></script>
94
+ {{/if}}
95
+ </body>
96
+ </html>
97
+ </template>
98
+
99
+ <logic>
100
+ # Vue.js SPA Integration Example
101
+ #
102
+ # Backend Usage (Ruby):
103
+ #
104
+ # view = Rhales::View.new(
105
+ # request,
106
+ # client: {
107
+ # ui: { theme: user.theme || 'light', locale: I18n.locale },
108
+ # authentication: { authenticated: logged_in?, userId: current_user&.id },
109
+ # user: current_user&.to_client_data,
110
+ # features: { darkMode: true, notifications: true },
111
+ # apiBaseUrl: ENV['API_BASE_URL']
112
+ # },
113
+ # server: {
114
+ # pageTitle: 'My App',
115
+ # viteAssetsHtml: vite_javascript_tag('application'),
116
+ # frontendHost: ENV['VITE_DEV_SERVER_URL'] # nil in production
117
+ # }
118
+ # )
119
+ #
120
+ # html = view.render('vue')
121
+ #
122
+ # Frontend Usage (Vue 3):
123
+ #
124
+ # // src/main.ts
125
+ # import { createApp } from 'vue'
126
+ # import App from './App.vue'
127
+ #
128
+ # // Access hydrated state (window variable name matches <schema window="...">)
129
+ # const state = window.__ONETIME_STATE__
130
+ #
131
+ # const app = createApp(App, {
132
+ # initialState: state
133
+ # })
134
+ #
135
+ # app.mount('#app')
136
+ </logic>