@constela/server 3.0.1 → 4.1.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.
- package/README.md +170 -87
- package/dist/index.d.ts +12 -0
- package/dist/index.js +38 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @constela/server
|
|
2
2
|
|
|
3
|
-
Server-side rendering (SSR) for
|
|
3
|
+
Server-side rendering (SSR) for Constela JSON programs.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,78 +11,127 @@ npm install @constela/server
|
|
|
11
11
|
**Peer Dependencies:**
|
|
12
12
|
- `@constela/compiler` ^0.7.0
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
JSON program → HTML string
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"version": "1.0",
|
|
21
|
+
"state": { "name": { "type": "string", "initial": "World" } },
|
|
22
|
+
"view": {
|
|
23
|
+
"kind": "element",
|
|
24
|
+
"tag": "h1",
|
|
25
|
+
"children": [
|
|
26
|
+
{ "kind": "text", "value": { "expr": "lit", "value": "Hello, " } },
|
|
27
|
+
{ "kind": "text", "value": { "expr": "state", "name": "name" } }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
15
32
|
|
|
16
|
-
|
|
33
|
+
↓ SSR
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **Dual Theme** - Light and dark theme code blocks
|
|
35
|
+
```html
|
|
36
|
+
<h1>Hello, World</h1>
|
|
37
|
+
```
|
|
22
38
|
|
|
23
|
-
##
|
|
39
|
+
## Features
|
|
24
40
|
|
|
25
|
-
###
|
|
41
|
+
### Markdown Rendering
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"kind": "markdown",
|
|
46
|
+
"content": { "expr": "data", "name": "article", "path": "content" }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
import { renderToString } from '@constela/server';
|
|
50
|
+
Rendered with async parsing and Shiki syntax highlighting.
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
});
|
|
52
|
+
### Code Highlighting
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"kind": "code",
|
|
57
|
+
"code": { "expr": "lit", "value": "const x = 1;" },
|
|
58
|
+
"language": { "expr": "lit", "value": "typescript" }
|
|
59
|
+
}
|
|
42
60
|
```
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
62
|
+
Features:
|
|
63
|
+
- Dual theme support (github-light, github-dark)
|
|
64
|
+
- CSS custom properties for theme switching
|
|
65
|
+
- Preloaded languages: javascript, typescript, json, html, css, python, rust, go, java, bash, markdown
|
|
66
|
+
|
|
67
|
+
### Route Context
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
Pass route parameters for dynamic pages:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"route": { "path": "/users/:id" },
|
|
74
|
+
"view": {
|
|
75
|
+
"kind": "text",
|
|
76
|
+
"value": { "expr": "route", "name": "id", "source": "param" }
|
|
77
|
+
}
|
|
57
78
|
}
|
|
58
79
|
```
|
|
59
80
|
|
|
60
|
-
|
|
81
|
+
### Import Data
|
|
61
82
|
|
|
62
|
-
|
|
83
|
+
Pass external data at render time:
|
|
63
84
|
|
|
64
|
-
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"imports": { "config": "./data/config.json" },
|
|
88
|
+
"view": {
|
|
89
|
+
"kind": "text",
|
|
90
|
+
"value": { "expr": "import", "name": "config", "path": "siteName" }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
65
94
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
95
|
+
### Style Evaluation
|
|
96
|
+
|
|
97
|
+
Style expressions are evaluated during SSR, producing CSS class strings:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"styles": {
|
|
102
|
+
"button": {
|
|
103
|
+
"base": "px-4 py-2 rounded",
|
|
104
|
+
"variants": {
|
|
105
|
+
"variant": {
|
|
106
|
+
"primary": "bg-blue-500 text-white",
|
|
107
|
+
"secondary": "bg-gray-200 text-gray-800"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"defaultVariants": { "variant": "primary" }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"view": {
|
|
114
|
+
"kind": "element",
|
|
115
|
+
"tag": "button",
|
|
116
|
+
"props": {
|
|
117
|
+
"className": {
|
|
118
|
+
"expr": "style",
|
|
119
|
+
"name": "button",
|
|
120
|
+
"variants": { "variant": { "expr": "lit", "value": "primary" } }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
69
126
|
|
|
70
|
-
|
|
71
|
-
- Dual theme support (github-light, github-dark)
|
|
72
|
-
- CSS custom properties for theme switching
|
|
73
|
-
- Preloaded languages: javascript, typescript, json, html, css, python, rust, go, java, bash, markdown
|
|
127
|
+
↓ SSR
|
|
74
128
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/* Light mode */
|
|
78
|
-
.shiki { background-color: var(--shiki-light-bg); }
|
|
79
|
-
.shiki span { color: var(--shiki-light); }
|
|
80
|
-
|
|
81
|
-
/* Dark mode */
|
|
82
|
-
.dark .shiki { background-color: var(--shiki-dark-bg); }
|
|
83
|
-
.dark .shiki span { color: var(--shiki-dark); }
|
|
129
|
+
```html
|
|
130
|
+
<button class="px-4 py-2 rounded bg-blue-500 text-white">...</button>
|
|
84
131
|
```
|
|
85
132
|
|
|
133
|
+
Pass style presets via `RenderOptions.styles` for evaluation.
|
|
134
|
+
|
|
86
135
|
## Output Structure
|
|
87
136
|
|
|
88
137
|
### Code Block HTML
|
|
@@ -91,23 +140,23 @@ The package internally handles:
|
|
|
91
140
|
<div class="constela-code" data-code-content="...">
|
|
92
141
|
<div class="group relative">
|
|
93
142
|
<div class="language-badge">typescript</div>
|
|
94
|
-
<button class="constela-copy-btn">
|
|
95
|
-
<!-- Copy icon SVG -->
|
|
96
|
-
</button>
|
|
143
|
+
<button class="constela-copy-btn"><!-- Copy icon --></button>
|
|
97
144
|
<pre><code class="shiki">...</code></pre>
|
|
98
145
|
</div>
|
|
99
146
|
</div>
|
|
100
147
|
```
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
## Expression Evaluation
|
|
149
|
+
### CSS Variables
|
|
105
150
|
|
|
106
|
-
|
|
151
|
+
```css
|
|
152
|
+
/* Light mode */
|
|
153
|
+
.shiki { background-color: var(--shiki-light-bg); }
|
|
154
|
+
.shiki span { color: var(--shiki-light); }
|
|
107
155
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
156
|
+
/* Dark mode */
|
|
157
|
+
.dark .shiki { background-color: var(--shiki-dark-bg); }
|
|
158
|
+
.dark .shiki span { color: var(--shiki-dark); }
|
|
159
|
+
```
|
|
111
160
|
|
|
112
161
|
## Security
|
|
113
162
|
|
|
@@ -115,29 +164,56 @@ SSR evaluates expressions server-side with some limitations:
|
|
|
115
164
|
- **DOMPurify** - Markdown content is sanitized
|
|
116
165
|
- **Prototype Pollution Prevention** - Same as runtime
|
|
117
166
|
|
|
118
|
-
##
|
|
167
|
+
## Internal API
|
|
168
|
+
|
|
169
|
+
> For framework developers only. End users should use the CLI.
|
|
170
|
+
|
|
171
|
+
### renderToString
|
|
119
172
|
|
|
120
173
|
```typescript
|
|
121
|
-
import { compile } from '@constela/compiler';
|
|
122
174
|
import { renderToString } from '@constela/server';
|
|
123
175
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
176
|
+
const html = await renderToString(compiledProgram, {
|
|
177
|
+
route: {
|
|
178
|
+
params: { id: '123' },
|
|
179
|
+
query: { tab: 'overview' },
|
|
180
|
+
path: '/users/123',
|
|
181
|
+
},
|
|
182
|
+
imports: {
|
|
183
|
+
config: { siteName: 'My Site' },
|
|
184
|
+
},
|
|
185
|
+
styles: {
|
|
186
|
+
button: {
|
|
187
|
+
base: 'px-4 py-2 rounded',
|
|
188
|
+
variants: {
|
|
189
|
+
variant: {
|
|
190
|
+
primary: 'bg-blue-500 text-white',
|
|
191
|
+
secondary: 'bg-gray-200 text-gray-800',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
defaultVariants: { variant: 'primary' },
|
|
195
|
+
},
|
|
135
196
|
},
|
|
136
197
|
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**RenderOptions:**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface RenderOptions {
|
|
204
|
+
route?: {
|
|
205
|
+
params?: Record<string, string>;
|
|
206
|
+
query?: Record<string, string>;
|
|
207
|
+
path?: string;
|
|
208
|
+
};
|
|
209
|
+
imports?: Record<string, unknown>;
|
|
210
|
+
styles?: Record<string, StylePreset>;
|
|
211
|
+
}
|
|
137
212
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
interface StylePreset {
|
|
214
|
+
base: string;
|
|
215
|
+
variants?: Record<string, Record<string, string>>;
|
|
216
|
+
defaultVariants?: Record<string, string>;
|
|
141
217
|
}
|
|
142
218
|
```
|
|
143
219
|
|
|
@@ -145,14 +221,21 @@ if (program.ok) {
|
|
|
145
221
|
|
|
146
222
|
Server-rendered HTML can be hydrated on the client:
|
|
147
223
|
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"version": "1.0",
|
|
227
|
+
"lifecycle": { "onMount": "initializeClient" },
|
|
228
|
+
"state": { ... },
|
|
229
|
+
"actions": [
|
|
230
|
+
{
|
|
231
|
+
"name": "initializeClient",
|
|
232
|
+
"steps": [
|
|
233
|
+
{ "do": "storage", "operation": "get", "key": { "expr": "lit", "value": "preferences" }, ... }
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
],
|
|
237
|
+
"view": { ... }
|
|
238
|
+
}
|
|
156
239
|
```
|
|
157
240
|
|
|
158
241
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ import { CompiledProgram } from '@constela/compiler';
|
|
|
6
6
|
* Renders CompiledProgram to HTML string for Server-Side Rendering.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Style preset definition for SSR
|
|
11
|
+
*/
|
|
12
|
+
interface StylePreset {
|
|
13
|
+
base: string;
|
|
14
|
+
variants?: Record<string, Record<string, string>>;
|
|
15
|
+
defaultVariants?: Record<string, string>;
|
|
16
|
+
compoundVariants?: Array<Record<string, string> & {
|
|
17
|
+
class: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
9
20
|
/**
|
|
10
21
|
* Options for renderToString
|
|
11
22
|
*/
|
|
@@ -16,6 +27,7 @@ interface RenderOptions {
|
|
|
16
27
|
path?: string;
|
|
17
28
|
};
|
|
18
29
|
imports?: Record<string, unknown>;
|
|
30
|
+
styles?: Record<string, StylePreset>;
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* Renders a CompiledProgram to an HTML string.
|
package/dist/index.js
CHANGED
|
@@ -213,6 +213,12 @@ function evaluate(expr, ctx) {
|
|
|
213
213
|
if (typeof key === "string" && forbiddenKeys.has(key)) return void 0;
|
|
214
214
|
return base[key];
|
|
215
215
|
}
|
|
216
|
+
case "param": {
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
case "style": {
|
|
220
|
+
return evaluateStyle(expr, ctx);
|
|
221
|
+
}
|
|
216
222
|
default: {
|
|
217
223
|
const _exhaustiveCheck = expr;
|
|
218
224
|
throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
|
|
@@ -304,6 +310,36 @@ function evaluateBinary(op, left, right, ctx) {
|
|
|
304
310
|
throw new Error("Unknown binary operator: " + op);
|
|
305
311
|
}
|
|
306
312
|
}
|
|
313
|
+
function evaluateStyle(expr, ctx) {
|
|
314
|
+
const preset = ctx.styles?.[expr.name];
|
|
315
|
+
if (!preset) return "";
|
|
316
|
+
let classes = preset.base;
|
|
317
|
+
if (preset.variants) {
|
|
318
|
+
for (const variantKey of Object.keys(preset.variants)) {
|
|
319
|
+
let variantValueStr = null;
|
|
320
|
+
if (expr.variants?.[variantKey]) {
|
|
321
|
+
let variantValue;
|
|
322
|
+
try {
|
|
323
|
+
variantValue = evaluate(expr.variants[variantKey], ctx);
|
|
324
|
+
} catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (variantValue != null) {
|
|
328
|
+
variantValueStr = String(variantValue);
|
|
329
|
+
}
|
|
330
|
+
} else if (preset.defaultVariants?.[variantKey] !== void 0) {
|
|
331
|
+
variantValueStr = preset.defaultVariants[variantKey];
|
|
332
|
+
}
|
|
333
|
+
if (variantValueStr !== null) {
|
|
334
|
+
const variantClasses = preset.variants[variantKey]?.[variantValueStr];
|
|
335
|
+
if (variantClasses) {
|
|
336
|
+
classes += " " + variantClasses;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return classes.trim();
|
|
342
|
+
}
|
|
307
343
|
function formatValue(value) {
|
|
308
344
|
if (value === null || value === void 0) {
|
|
309
345
|
return "";
|
|
@@ -432,7 +468,8 @@ async function renderToString(program, options) {
|
|
|
432
468
|
query: options.route.query ?? {},
|
|
433
469
|
path: options.route.path ?? ""
|
|
434
470
|
} : void 0,
|
|
435
|
-
imports: options?.imports ?? program.importData
|
|
471
|
+
imports: options?.imports ?? program.importData,
|
|
472
|
+
styles: options?.styles
|
|
436
473
|
};
|
|
437
474
|
return await renderNode(program.view, ctx);
|
|
438
475
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Server-side rendering for Constela UI framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@constela/compiler": "^0.
|
|
18
|
+
"@constela/compiler": "^0.8.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"isomorphic-dompurify": "^2.35.0",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"tsup": "^8.0.0",
|
|
29
29
|
"typescript": "^5.3.0",
|
|
30
30
|
"vitest": "^2.0.0",
|
|
31
|
-
"@constela/compiler": "0.
|
|
31
|
+
"@constela/compiler": "0.8.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=20.0.0"
|