driftjs-ssr 0.0.1
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/dist/index-es.js +6973 -0
- package/package.json +26 -0
- package/src/index.ts +334 -0
- package/tests/ssr.test.ts +153 -0
- package/types/index.ts +11 -0
- package/vite.config.ts +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "driftjs-ssr",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index-es.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index-es.js",
|
|
9
|
+
"require": "./dist/index-cjs.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"driftjs-compiler": "0.0.1",
|
|
15
|
+
"driftjs-shared": "0.0.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^7.0.2",
|
|
19
|
+
"vite": "^8.1.5",
|
|
20
|
+
"vitest": "^4.1.10"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "vite build",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { type CompiledModule, Opcode } from "driftjs-compiler";
|
|
2
|
+
import {
|
|
3
|
+
evaluateExpression,
|
|
4
|
+
executeBlockStatement,
|
|
5
|
+
resolveComponentModule,
|
|
6
|
+
evaluatePropsSpec,
|
|
7
|
+
} from "driftjs-shared";
|
|
8
|
+
import type { SSRExecutionOptions, ServerNode } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
export * from "../types/index.js";
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Escapes special HTML characters to prevent XSS.
|
|
15
|
+
*/
|
|
16
|
+
function escapeHtml(str: string): string {
|
|
17
|
+
return str
|
|
18
|
+
.replace(/&/g, "&")
|
|
19
|
+
.replace(/</g, "<")
|
|
20
|
+
.replace(/>/g, ">")
|
|
21
|
+
.replace(/"/g, """)
|
|
22
|
+
.replace(/'/g, "'");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register-based Virtual Machine for Server-Side Rendering (SSR) in DriftJS.
|
|
27
|
+
* Executes bytecode without DOM dependencies and serializes directly to HTML string.
|
|
28
|
+
*/
|
|
29
|
+
export class DriftServerVM {
|
|
30
|
+
private static readonly MAX_REGISTERS = 256;
|
|
31
|
+
private readonly registers: ServerNode[] = new Array(DriftServerVM.MAX_REGISTERS);
|
|
32
|
+
private scope: Record<string, any> = {};
|
|
33
|
+
private declaredVars: Set<string> = new Set();
|
|
34
|
+
|
|
35
|
+
private checkRegister(index: number): void {
|
|
36
|
+
if (index < 0 || index >= DriftServerVM.MAX_REGISTERS) {
|
|
37
|
+
throw new Error(`Register index ${index} out of bounds (0-${DriftServerVM.MAX_REGISTERS - 1})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private setRegister(index: number, value: ServerNode): void {
|
|
42
|
+
this.checkRegister(index);
|
|
43
|
+
this.registers[index] = value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getRegister(index: number): any {
|
|
47
|
+
this.checkRegister(index);
|
|
48
|
+
return this.registers[index];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public execute(rawModule: CompiledModule, options: SSRExecutionOptions = {}): ServerNode | null {
|
|
52
|
+
const module = (resolveComponentModule(rawModule) || rawModule) as CompiledModule;
|
|
53
|
+
this.scope = options.scope ? options.scope : { ...module.scope };
|
|
54
|
+
if (module.scope) {
|
|
55
|
+
for (const k of Object.keys(module.scope)) {
|
|
56
|
+
if (!Object.prototype.hasOwnProperty.call(this.scope, k)) {
|
|
57
|
+
this.scope[k] = module.scope[k];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.declaredVars = new Set(module.declaredVars ?? []);
|
|
62
|
+
this.registers.fill(null as any);
|
|
63
|
+
|
|
64
|
+
const bytecode = module.bytecode;
|
|
65
|
+
const constants = module.constants;
|
|
66
|
+
let pc = 0;
|
|
67
|
+
|
|
68
|
+
while (pc < bytecode.length) {
|
|
69
|
+
const opcode = bytecode[pc]!;
|
|
70
|
+
|
|
71
|
+
switch (opcode) {
|
|
72
|
+
case Opcode.RETURN: {
|
|
73
|
+
const reg = bytecode[pc + 1]!;
|
|
74
|
+
return this.getRegister(reg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case Opcode.CREATE_ELEMENT: {
|
|
78
|
+
const dstReg = bytecode[pc + 1]!;
|
|
79
|
+
const tagIdx = bytecode[pc + 2]!;
|
|
80
|
+
const tag = String(constants[tagIdx]);
|
|
81
|
+
const maybePropsIdx = pc + 3 < bytecode.length ? bytecode[pc + 3]! : 0xFF;
|
|
82
|
+
const propsCandidate = (maybePropsIdx !== 0xFF && maybePropsIdx < constants.length) ? constants[maybePropsIdx] : null;
|
|
83
|
+
const isPropsSpec = propsCandidate && typeof propsCandidate === 'object' && propsCandidate.__drift_props__ === true;
|
|
84
|
+
const propsSpecIdx = isPropsSpec ? maybePropsIdx : 0xFF;
|
|
85
|
+
|
|
86
|
+
const rawComp = (this.scope && tag in this.scope) ? this.scope[tag] : (typeof globalThis !== 'undefined' && (globalThis as any)[tag]);
|
|
87
|
+
const compMod = resolveComponentModule(rawComp);
|
|
88
|
+
if (compMod) {
|
|
89
|
+
const propsSpec = propsSpecIdx !== 0xFF ? constants[propsSpecIdx] : null;
|
|
90
|
+
const propsObj = evaluatePropsSpec(propsSpec, this.scope, this.declaredVars);
|
|
91
|
+
const subVm = new DriftServerVM();
|
|
92
|
+
const compNode = subVm.execute(compMod, { scope: { props: propsObj, ...propsObj, ...this.scope } });
|
|
93
|
+
if (compNode) this.setRegister(dstReg, compNode);
|
|
94
|
+
} else {
|
|
95
|
+
this.setRegister(dstReg, {
|
|
96
|
+
type: 'element',
|
|
97
|
+
tag,
|
|
98
|
+
attrs: new Map(),
|
|
99
|
+
children: [],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
pc += isPropsSpec ? 4 : 3;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case Opcode.CREATE_TEXT: {
|
|
107
|
+
const dstReg = bytecode[pc + 1]!;
|
|
108
|
+
const textIdx = bytecode[pc + 2]!;
|
|
109
|
+
const content = String(constants[textIdx]);
|
|
110
|
+
this.setRegister(dstReg, {
|
|
111
|
+
type: 'text',
|
|
112
|
+
content,
|
|
113
|
+
children: [],
|
|
114
|
+
});
|
|
115
|
+
pc += 3;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case Opcode.CREATE_COMMENT: {
|
|
120
|
+
const dstReg = bytecode[pc + 1]!;
|
|
121
|
+
const commentIdx = bytecode[pc + 2]!;
|
|
122
|
+
const content = String(constants[commentIdx]);
|
|
123
|
+
this.setRegister(dstReg, {
|
|
124
|
+
type: 'comment',
|
|
125
|
+
content,
|
|
126
|
+
children: [],
|
|
127
|
+
});
|
|
128
|
+
pc += 3;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case Opcode.CREATE_FRAGMENT: {
|
|
133
|
+
const dstReg = bytecode[pc + 1]!;
|
|
134
|
+
this.setRegister(dstReg, {
|
|
135
|
+
type: 'fragment',
|
|
136
|
+
children: [],
|
|
137
|
+
});
|
|
138
|
+
pc += 2;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case Opcode.APPEND_CHILD: {
|
|
143
|
+
const parentReg = bytecode[pc + 1]!;
|
|
144
|
+
const childReg = bytecode[pc + 2]!;
|
|
145
|
+
const parentNode = this.getRegister(parentReg);
|
|
146
|
+
const childNode = this.getRegister(childReg);
|
|
147
|
+
parentNode.children.push(childNode);
|
|
148
|
+
pc += 3;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case Opcode.SET_ATTR: {
|
|
153
|
+
const elemReg = bytecode[pc + 1]!;
|
|
154
|
+
const nameIdx = bytecode[pc + 2]!;
|
|
155
|
+
const valIdx = bytecode[pc + 3]!;
|
|
156
|
+
const isDynamic = bytecode[pc + 4]!;
|
|
157
|
+
|
|
158
|
+
const elemNode = this.getRegister(elemReg);
|
|
159
|
+
const attrName = String(constants[nameIdx]);
|
|
160
|
+
if (attrName.startsWith('on')) {
|
|
161
|
+
pc += 5;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const rawVal = constants[valIdx];
|
|
166
|
+
const val = isDynamic === 1 ? evaluateExpression(rawVal, this.scope, this.declaredVars) : rawVal;
|
|
167
|
+
|
|
168
|
+
if (!elemNode.attrs) elemNode.attrs = new Map();
|
|
169
|
+
elemNode.attrs.set(attrName, val);
|
|
170
|
+
pc += 5;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case Opcode.INTERPOLATE_TEXT: {
|
|
175
|
+
const dstReg = bytecode[pc + 1]!;
|
|
176
|
+
const exprIdx = bytecode[pc + 2]!;
|
|
177
|
+
const expr = constants[exprIdx];
|
|
178
|
+
const val = evaluateExpression(expr, this.scope, this.declaredVars);
|
|
179
|
+
const content = val != null ? String(val) : '';
|
|
180
|
+
this.setRegister(dstReg, {
|
|
181
|
+
type: 'text',
|
|
182
|
+
content,
|
|
183
|
+
children: [],
|
|
184
|
+
});
|
|
185
|
+
pc += 3;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case Opcode.EVAL_EXPR: {
|
|
190
|
+
const dstReg = bytecode[pc + 1]!;
|
|
191
|
+
const exprIdx = bytecode[pc + 2]!;
|
|
192
|
+
const expr = constants[exprIdx];
|
|
193
|
+
this.setRegister(dstReg, evaluateExpression(expr, this.scope, this.declaredVars));
|
|
194
|
+
pc += 3;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case Opcode.JUMP: {
|
|
199
|
+
pc = (bytecode[pc + 1]! << 8) | bytecode[pc + 2]!;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case Opcode.JUMP_IF_FALSE: {
|
|
204
|
+
const cond = this.getRegister(bytecode[pc + 1]!);
|
|
205
|
+
if (!cond) {
|
|
206
|
+
pc = (bytecode[pc + 2]! << 8) | bytecode[pc + 3]!;
|
|
207
|
+
} else {
|
|
208
|
+
pc += 4;
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case Opcode.EXEC_SCRIPT: {
|
|
214
|
+
const scriptIdx = bytecode[pc + 1]!;
|
|
215
|
+
const scriptAst = constants[scriptIdx];
|
|
216
|
+
if (Array.isArray(scriptAst)) {
|
|
217
|
+
executeBlockStatement(scriptAst, this.scope, this.declaredVars);
|
|
218
|
+
} else if (scriptAst && typeof scriptAst === 'object') {
|
|
219
|
+
executeBlockStatement([scriptAst], this.scope, this.declaredVars);
|
|
220
|
+
}
|
|
221
|
+
pc += 2;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case Opcode.REACTIVE_IF: {
|
|
226
|
+
const parentReg = bytecode[pc + 1]!;
|
|
227
|
+
const condIdx = bytecode[pc + 2]!;
|
|
228
|
+
const consIdx = bytecode[pc + 3]!;
|
|
229
|
+
const altIdx = bytecode[pc + 4]!;
|
|
230
|
+
|
|
231
|
+
const parentNode = this.getRegister(parentReg);
|
|
232
|
+
const condExpr = constants[condIdx];
|
|
233
|
+
const consMod = constants[consIdx];
|
|
234
|
+
const altMod = altIdx !== 0xFF ? constants[altIdx] : null;
|
|
235
|
+
|
|
236
|
+
const cond = evaluateExpression(condExpr, this.scope, this.declaredVars);
|
|
237
|
+
const subMod = cond ? consMod : altMod;
|
|
238
|
+
|
|
239
|
+
parentNode.children.push({ type: 'comment', content: 'if', children: [] });
|
|
240
|
+
if (subMod) {
|
|
241
|
+
const subVm = new DriftServerVM();
|
|
242
|
+
const subResult = subVm.execute(subMod, { scope: this.scope });
|
|
243
|
+
if (subResult) parentNode.children.push(subResult);
|
|
244
|
+
}
|
|
245
|
+
parentNode.children.push({ type: 'comment', content: '/if', children: [] });
|
|
246
|
+
pc += 6;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case Opcode.REACTIVE_FOR: {
|
|
251
|
+
const parentReg = bytecode[pc + 1]!;
|
|
252
|
+
const iterIdx = bytecode[pc + 2]!;
|
|
253
|
+
const itemNameIdx = bytecode[pc + 3]!;
|
|
254
|
+
const idxNameIdx = bytecode[pc + 4]!;
|
|
255
|
+
const keyIdx = bytecode[pc + 5]!;
|
|
256
|
+
const bodyIdx = bytecode[pc + 6]!;
|
|
257
|
+
|
|
258
|
+
const parentNode = this.getRegister(parentReg);
|
|
259
|
+
const iterExpr = constants[iterIdx];
|
|
260
|
+
const itemName = constants[itemNameIdx] as string;
|
|
261
|
+
const indexName = idxNameIdx !== 0xFF ? constants[idxNameIdx] as string : null;
|
|
262
|
+
const bodyMod = constants[bodyIdx];
|
|
263
|
+
|
|
264
|
+
const rawIter = evaluateExpression(iterExpr, this.scope, this.declaredVars);
|
|
265
|
+
const items = Array.isArray(rawIter)
|
|
266
|
+
? rawIter
|
|
267
|
+
: rawIter && typeof rawIter[Symbol.iterator] === 'function'
|
|
268
|
+
? Array.from(rawIter)
|
|
269
|
+
: [];
|
|
270
|
+
|
|
271
|
+
parentNode.children.push({ type: 'comment', content: 'for', children: [] });
|
|
272
|
+
for (let i = 0; i < items.length; i++) {
|
|
273
|
+
const childScope = Object.create(this.scope);
|
|
274
|
+
childScope[itemName] = items[i];
|
|
275
|
+
if (indexName) childScope[indexName] = i;
|
|
276
|
+
|
|
277
|
+
const subVm = new DriftServerVM();
|
|
278
|
+
const subResult = subVm.execute(bodyMod, { scope: childScope });
|
|
279
|
+
if (subResult) parentNode.children.push(subResult);
|
|
280
|
+
}
|
|
281
|
+
parentNode.children.push({ type: 'comment', content: '/for', children: [] });
|
|
282
|
+
pc += 8;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
default:
|
|
287
|
+
throw new Error(`DriftServerVM: Unknown Opcode ${opcode} at PC ${pc}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Serializes a ServerNode tree directly into an HTML string.
|
|
297
|
+
*/
|
|
298
|
+
export function serializeNode(node: ServerNode | string): string {
|
|
299
|
+
if (typeof node === 'string') return escapeHtml(node);
|
|
300
|
+
if (node.type === 'text') return escapeHtml(node.content ?? '');
|
|
301
|
+
if (node.type === 'comment') return `<!--${node.content ?? ''}-->`;
|
|
302
|
+
if (node.type === 'fragment') {
|
|
303
|
+
return node.children.map(serializeNode).join('');
|
|
304
|
+
}
|
|
305
|
+
if (node.type === 'element') {
|
|
306
|
+
const tag = node.tag!;
|
|
307
|
+
let attrsStr = '';
|
|
308
|
+
if (node.attrs && node.attrs.size > 0) {
|
|
309
|
+
for (const [k, v] of node.attrs.entries()) {
|
|
310
|
+
if (v === '' || v === true) {
|
|
311
|
+
attrsStr += ` ${k}`;
|
|
312
|
+
} else if (v !== null && v !== undefined && v !== false) {
|
|
313
|
+
attrsStr += ` ${k}="${escapeHtml(String(v))}"`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const selfClosing = ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tag.toLowerCase());
|
|
318
|
+
if (selfClosing) {
|
|
319
|
+
return `<${tag}${attrsStr} />`;
|
|
320
|
+
}
|
|
321
|
+
const childrenStr = node.children.map(serializeNode).join('');
|
|
322
|
+
return `<${tag}${attrsStr}>${childrenStr}</${tag}>`;
|
|
323
|
+
}
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Renders a compiled Drift component to an HTML string.
|
|
329
|
+
*/
|
|
330
|
+
export function renderToString(component: CompiledModule, options: SSRExecutionOptions = {}): string {
|
|
331
|
+
const vm = new DriftServerVM();
|
|
332
|
+
const rootNode = vm.execute(component, options);
|
|
333
|
+
return rootNode ? serializeNode(rootNode) : '';
|
|
334
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DriftServerVM, renderToString } from '../src/index.js';
|
|
3
|
+
import { Opcode, CompiledModule } from 'driftjs-compiler';
|
|
4
|
+
|
|
5
|
+
describe('DriftServerVM (SSR Engine)', () => {
|
|
6
|
+
it('renders static elements with escape protection to HTML string', () => {
|
|
7
|
+
const module: CompiledModule = {
|
|
8
|
+
bytecode: [
|
|
9
|
+
Opcode.CREATE_ELEMENT, 0, 0, // r0 = h1
|
|
10
|
+
Opcode.CREATE_TEXT, 1, 1, // r1 = 'Hello <World> & "Friends"'
|
|
11
|
+
Opcode.APPEND_CHILD, 0, 1,
|
|
12
|
+
Opcode.RETURN, 0,
|
|
13
|
+
],
|
|
14
|
+
constants: ['h1', 'Hello <World> & "Friends"'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const html = renderToString(module);
|
|
18
|
+
expect(html).toBe('<h1>Hello <World> & "Friends"</h1>');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders attributes, boolean flags, and dynamic values', () => {
|
|
22
|
+
const module: CompiledModule = {
|
|
23
|
+
bytecode: [
|
|
24
|
+
Opcode.CREATE_ELEMENT, 0, 0, // r0 = input
|
|
25
|
+
Opcode.SET_ATTR, 0, 1, 2, 0, // type="checkbox"
|
|
26
|
+
Opcode.SET_ATTR, 0, 3, 4, 0, // checked=true
|
|
27
|
+
Opcode.SET_ATTR, 0, 5, 6, 1, // data-id=eval(id)
|
|
28
|
+
Opcode.RETURN, 0,
|
|
29
|
+
],
|
|
30
|
+
constants: [
|
|
31
|
+
'input', 'type', 'checkbox', 'checked', true, 'data-id',
|
|
32
|
+
{ type: 'Identifier', name: 'id' },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const html = renderToString(module, { scope: { id: 101 } });
|
|
37
|
+
expect(html).toBe('<input type="checkbox" checked data-id="101" />');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders REACTIVE_IF conditionals on server (true branch & false branch)', () => {
|
|
41
|
+
const consMod: CompiledModule = {
|
|
42
|
+
bytecode: [
|
|
43
|
+
Opcode.CREATE_FRAGMENT, 0,
|
|
44
|
+
Opcode.CREATE_ELEMENT, 1, 0,
|
|
45
|
+
Opcode.CREATE_TEXT, 2, 1,
|
|
46
|
+
Opcode.APPEND_CHILD, 1, 2,
|
|
47
|
+
Opcode.APPEND_CHILD, 0, 1,
|
|
48
|
+
Opcode.RETURN, 0,
|
|
49
|
+
],
|
|
50
|
+
constants: ['span', 'User Admin'],
|
|
51
|
+
};
|
|
52
|
+
const altMod: CompiledModule = {
|
|
53
|
+
bytecode: [
|
|
54
|
+
Opcode.CREATE_FRAGMENT, 0,
|
|
55
|
+
Opcode.CREATE_ELEMENT, 1, 0,
|
|
56
|
+
Opcode.CREATE_TEXT, 2, 1,
|
|
57
|
+
Opcode.APPEND_CHILD, 1, 2,
|
|
58
|
+
Opcode.APPEND_CHILD, 0, 1,
|
|
59
|
+
Opcode.RETURN, 0,
|
|
60
|
+
],
|
|
61
|
+
constants: ['span', 'Guest User'],
|
|
62
|
+
};
|
|
63
|
+
const condExpr = { type: 'Identifier', name: 'isAdmin' };
|
|
64
|
+
|
|
65
|
+
const module: CompiledModule = {
|
|
66
|
+
bytecode: [
|
|
67
|
+
Opcode.CREATE_FRAGMENT, 0,
|
|
68
|
+
Opcode.REACTIVE_IF, 0, 1, 2, 3, 4,
|
|
69
|
+
Opcode.RETURN, 0,
|
|
70
|
+
],
|
|
71
|
+
constants: [null, condExpr, consMod, altMod, ['isAdmin']],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const adminHtml = renderToString(module, { scope: { isAdmin: true } });
|
|
75
|
+
expect(adminHtml).toBe('<!--if--><span>User Admin</span><!--/if-->');
|
|
76
|
+
|
|
77
|
+
const guestHtml = renderToString(module, { scope: { isAdmin: false } });
|
|
78
|
+
expect(guestHtml).toBe('<!--if--><span>Guest User</span><!--/if-->');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders REACTIVE_FOR loops with item and index scope bindings', () => {
|
|
82
|
+
const bodyMod: CompiledModule = {
|
|
83
|
+
bytecode: [
|
|
84
|
+
Opcode.CREATE_FRAGMENT, 0,
|
|
85
|
+
Opcode.CREATE_ELEMENT, 1, 0, // li
|
|
86
|
+
Opcode.SET_ATTR, 1, 1, 2, 1, // data-index={idx}
|
|
87
|
+
Opcode.INTERPOLATE_TEXT, 3, 4, // text={item}
|
|
88
|
+
Opcode.APPEND_CHILD, 1, 3,
|
|
89
|
+
Opcode.APPEND_CHILD, 0, 1,
|
|
90
|
+
Opcode.RETURN, 0,
|
|
91
|
+
],
|
|
92
|
+
constants: [
|
|
93
|
+
'li',
|
|
94
|
+
'data-index',
|
|
95
|
+
{ type: 'Identifier', name: 'idx' },
|
|
96
|
+
null,
|
|
97
|
+
{ type: 'Identifier', name: 'item' },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const module: CompiledModule = {
|
|
102
|
+
bytecode: [
|
|
103
|
+
Opcode.CREATE_ELEMENT, 0, 0, // ul
|
|
104
|
+
Opcode.REACTIVE_FOR, 0, 1, 2, 3, 0xFF, 4, 5,
|
|
105
|
+
Opcode.RETURN, 0,
|
|
106
|
+
],
|
|
107
|
+
constants: [
|
|
108
|
+
'ul',
|
|
109
|
+
{ type: 'Identifier', name: 'items' },
|
|
110
|
+
'item',
|
|
111
|
+
'idx',
|
|
112
|
+
bodyMod,
|
|
113
|
+
['items'],
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const html = renderToString(module, { scope: { items: ['Alpha', 'Beta'] } });
|
|
118
|
+
expect(html).toBe('<ul><!--for--><li data-index="0">Alpha</li><li data-index="1">Beta</li><!--/for--></ul>');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('EXEC_SCRIPT initialises server scope before rendering HTML', () => {
|
|
122
|
+
const scriptAst = [
|
|
123
|
+
{
|
|
124
|
+
type: 'VariableDeclaration',
|
|
125
|
+
declarations: [
|
|
126
|
+
{
|
|
127
|
+
type: 'VariableDeclarator',
|
|
128
|
+
id: { type: 'Identifier', name: 'title' },
|
|
129
|
+
init: { type: 'Literal', value: 'SSR Server Heading' },
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const module: CompiledModule = {
|
|
136
|
+
bytecode: [
|
|
137
|
+
Opcode.EXEC_SCRIPT, 0,
|
|
138
|
+
Opcode.CREATE_ELEMENT, 1, 1,
|
|
139
|
+
Opcode.INTERPOLATE_TEXT, 2, 2,
|
|
140
|
+
Opcode.APPEND_CHILD, 1, 2,
|
|
141
|
+
Opcode.RETURN, 1,
|
|
142
|
+
],
|
|
143
|
+
constants: [
|
|
144
|
+
scriptAst,
|
|
145
|
+
'h2',
|
|
146
|
+
{ type: 'Identifier', name: 'title' },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const html = renderToString(module);
|
|
151
|
+
expect(html).toBe('<h2>SSR Server Heading</h2>');
|
|
152
|
+
});
|
|
153
|
+
});
|
package/types/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SSRExecutionOptions {
|
|
2
|
+
readonly scope?: Record<string, any>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ServerNode {
|
|
6
|
+
type: "element" | "text" | "comment" | "fragment";
|
|
7
|
+
tag?: string;
|
|
8
|
+
attrs?: Map<string, string | boolean | null>;
|
|
9
|
+
children: (ServerNode | string)[];
|
|
10
|
+
content?: string;
|
|
11
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
lib: {
|
|
7
|
+
entry: path.resolve(__dirname, 'src/index.ts'),
|
|
8
|
+
name: 'DriftSSR',
|
|
9
|
+
fileName: (format) => `index-${format}.js`,
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
external: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|