ripple 0.2.108 → 0.2.110

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.108",
6
+ "version": "0.2.110",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -165,18 +165,19 @@ function RipplePlugin(config) {
165
165
 
166
166
  // Check if this is #server
167
167
  if (this.input.slice(this.pos, this.pos + 7) === '#server') {
168
- // Check that next char after 'server' is whitespace or {
168
+ // Check that next char after 'server' is whitespace, {, . (dot), or EOF
169
169
  const charAfter =
170
170
  this.pos + 7 < this.input.length ? this.input.charCodeAt(this.pos + 7) : -1;
171
171
  if (
172
- charAfter === 123 ||
173
- charAfter === 32 ||
174
- charAfter === 9 ||
175
- charAfter === 10 ||
176
- charAfter === 13 ||
177
- charAfter === -1
172
+ charAfter === 123 || // {
173
+ charAfter === 46 || // . (dot)
174
+ charAfter === 32 || // space
175
+ charAfter === 9 || // tab
176
+ charAfter === 10 || // newline
177
+ charAfter === 13 || // carriage return
178
+ charAfter === -1 // EOF
178
179
  ) {
179
- // { or whitespace or EOF
180
+ // { or . or whitespace or EOF
180
181
  this.pos += 7; // consume '#server'
181
182
  return this.finishToken(tt.name, '#server');
182
183
  }
@@ -379,6 +380,13 @@ function RipplePlugin(config) {
379
380
  return this.parseTrackedExpression();
380
381
  }
381
382
 
383
+ // Check if this is #server identifier for server function calls
384
+ if (this.type === tt.name && this.value === '#server') {
385
+ const node = this.startNode();
386
+ this.next();
387
+ return this.finishNode(node, 'ServerIdentifier');
388
+ }
389
+
382
390
  // Check if this is a tuple literal starting with #[
383
391
  if (this.type === tt.bracketL && this.value === '#[') {
384
392
  return this.parseTrackedArrayExpression();
@@ -388,7 +396,6 @@ function RipplePlugin(config) {
388
396
 
389
397
  return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
390
398
  }
391
-
392
399
  /**
393
400
  * Parse `@(expression)` syntax for unboxing tracked values
394
401
  * Creates a TrackedExpression node with the argument property
@@ -419,7 +426,21 @@ function RipplePlugin(config) {
419
426
  parseServerBlock() {
420
427
  const node = this.startNode();
421
428
  this.next();
422
- node.body = this.parseBlock();
429
+
430
+ const body = this.startNode();
431
+ node.body = body;
432
+ body.body = [];
433
+
434
+ this.expect(tt.braceL);
435
+ this.enterScope(0);
436
+ while (this.type !== tt.braceR) {
437
+ var stmt = this.parseStatement(null, true);
438
+ body.body.push(stmt);
439
+ }
440
+ this.next();
441
+ this.exitScope();
442
+ this.finishNode(body, 'BlockStatement');
443
+
423
444
  this.awaitPos = 0;
424
445
  return this.finishNode(node, 'ServerBlock');
425
446
  }
@@ -96,11 +96,46 @@ const visitors = {
96
96
  return context.next({ ...context.state, function_depth: 0, expression: null });
97
97
  },
98
98
 
99
+ ServerBlock(node, context) {
100
+ node.metadata = {
101
+ ...node.metadata,
102
+ exports: [],
103
+ };
104
+ context.visit(node.body, { ...context.state, inside_server_block: true });
105
+ },
106
+
99
107
  Identifier(node, context) {
100
108
  const binding = context.state.scope.get(node.name);
101
109
  const parent = context.path.at(-1);
102
110
 
103
- if (binding?.kind === 'prop' || binding?.kind === 'prop_fallback') {
111
+ if (
112
+ is_reference(node, /** @type {Node} */ (parent)) &&
113
+ binding &&
114
+ context.state.inside_server_block
115
+ ) {
116
+ let current_scope = binding.scope;
117
+
118
+ while (current_scope !== null) {
119
+ if (current_scope.server_block) {
120
+ break;
121
+ }
122
+ const parent_scope = current_scope.parent;
123
+ if (parent_scope === null) {
124
+ error(
125
+ `Cannot reference client-side variable "${node.name}" from a server block`,
126
+ context.state.analysis.module.filename,
127
+ node,
128
+ );
129
+ }
130
+ current_scope = parent_scope;
131
+ }
132
+ }
133
+
134
+ if (
135
+ binding?.kind === 'prop' ||
136
+ binding?.kind === 'prop_fallback' ||
137
+ binding?.kind === 'for_pattern'
138
+ ) {
104
139
  mark_as_tracked(context.path);
105
140
  if (context.state.metadata?.tracking === false) {
106
141
  context.state.metadata.tracking = true;
@@ -312,6 +347,7 @@ const visitors = {
312
347
  }
313
348
 
314
349
  node.metadata = {
350
+ ...node.metadata,
315
351
  has_template: false,
316
352
  };
317
353
 
@@ -347,7 +383,38 @@ const visitors = {
347
383
  }
348
384
  }
349
385
 
386
+ if (node.key) {
387
+ const state = context.state;
388
+ const pattern = node.left.declarations[0].id;
389
+ const paths = extract_paths(pattern);
390
+ const scope = state.scopes.get(node);
391
+ const pattern_id = b.id(scope.generate('pattern'));
392
+
393
+ node.left.declarations[0].id = pattern_id;
394
+
395
+ for (const path of paths) {
396
+ const name = path.node.name;
397
+ const binding = context.state.scope.get(name);
398
+
399
+ binding.kind = 'for_pattern';
400
+ if (!binding.metadata) {
401
+ binding.metadata = {
402
+ pattern: pattern_id,
403
+ };
404
+ }
405
+
406
+ if (binding !== null) {
407
+ binding.transform = {
408
+ read: () => {
409
+ return path.expression(b.call('_$_.get', pattern_id));
410
+ },
411
+ };
412
+ }
413
+ }
414
+ }
415
+
350
416
  node.metadata = {
417
+ ...node.metadata,
351
418
  has_template: false,
352
419
  };
353
420
  context.next();
@@ -361,6 +428,23 @@ const visitors = {
361
428
  }
362
429
  },
363
430
 
431
+ ExportNamedDeclaration(node, context) {
432
+ if (!context.state.inside_server_block) {
433
+ return context.next();
434
+ }
435
+ const server_block = context.path.find((n) => n.type === 'ServerBlock');
436
+ const declaration = node.declaration;
437
+
438
+ if (declaration && declaration.type === 'FunctionDeclaration') {
439
+ server_block.metadata.exports.push(declaration.id.name);
440
+ } else {
441
+ // TODO
442
+ throw new Error('Not implemented');
443
+ }
444
+
445
+ return context.next();
446
+ },
447
+
364
448
  TSTypeReference(node, context) {
365
449
  // bug in our acorn pasrer: it uses typeParameters instead of typeArguments
366
450
  if (node.typeParameters) {
@@ -376,6 +460,7 @@ const visitors = {
376
460
  }
377
461
 
378
462
  node.metadata = {
463
+ ...node.metadata,
379
464
  has_template: false,
380
465
  };
381
466
 
@@ -391,6 +476,7 @@ const visitors = {
391
476
 
392
477
  if (node.alternate) {
393
478
  node.metadata = {
479
+ ...node.metadata,
394
480
  has_template: false,
395
481
  };
396
482
  context.visit(node.alternate, context.state);
@@ -417,6 +503,7 @@ const visitors = {
417
503
  }
418
504
 
419
505
  node.metadata = {
506
+ ...node.metadata,
420
507
  has_template: false,
421
508
  };
422
509
 
@@ -431,6 +518,7 @@ const visitors = {
431
518
  }
432
519
 
433
520
  node.metadata = {
521
+ ...node.metadata,
434
522
  has_template: false,
435
523
  };
436
524
 
@@ -672,6 +760,7 @@ export function analyze(ast, filename) {
672
760
  scopes,
673
761
  analysis,
674
762
  inside_head: false,
763
+ inside_server_block: false,
675
764
  },
676
765
  visitors,
677
766
  );
@@ -1,4 +1,4 @@
1
- /** @import {Expression, FunctionExpression, Pattern, Node, Program} from 'estree' */
1
+ /** @import {Expression, FunctionExpression, Node, Program} from 'estree' */
2
2
 
3
3
  import { walk } from 'zimmerframe';
4
4
  import path from 'node:path';
@@ -37,6 +37,7 @@ import is_reference from 'is-reference';
37
37
  import { object } from '../../../../utils/ast.js';
38
38
  import { render_stylesheets } from '../stylesheet.js';
39
39
  import { is_event_attribute, is_passive_event } from '../../../../utils/events.js';
40
+ import { createHash } from 'node:crypto';
40
41
 
41
42
  function add_ripple_internal_import(context) {
42
43
  if (!context.state.to_ts) {
@@ -54,6 +55,7 @@ function visit_function(node, context) {
54
55
  const state = context.state;
55
56
 
56
57
  delete node.returnType;
58
+ delete node.typeParameters;
57
59
 
58
60
  for (const param of node.params) {
59
61
  delete param.typeAnnotation;
@@ -168,7 +170,8 @@ const visitors = {
168
170
  (node.tracked ||
169
171
  binding?.kind === 'prop' ||
170
172
  binding?.kind === 'index' ||
171
- binding?.kind === 'prop_fallback') &&
173
+ binding?.kind === 'prop_fallback' ||
174
+ binding?.kind === 'for_pattern') &&
172
175
  binding?.node !== node
173
176
  ) {
174
177
  if (context.state.metadata?.tracking === false) {
@@ -178,14 +181,17 @@ const visitors = {
178
181
  add_ripple_internal_import(context);
179
182
  return b.call('_$_.get', build_getter(node, context));
180
183
  }
181
-
182
- add_ripple_internal_import(context);
183
- return build_getter(node, context);
184
184
  }
185
+ add_ripple_internal_import(context);
186
+ return build_getter(node, context);
185
187
  }
186
188
  }
187
189
  },
188
190
 
191
+ ServerIdentifier(node, context) {
192
+ return b.id('_$_server_$_');
193
+ },
194
+
189
195
  ImportDeclaration(node, context) {
190
196
  if (!context.state.to_ts && node.importKind === 'type') {
191
197
  return b.empty;
@@ -1200,6 +1206,14 @@ const visitors = {
1200
1206
  return context.next();
1201
1207
  },
1202
1208
 
1209
+ TSInstantiationExpression(node, context) {
1210
+ if (!context.state.to_ts) {
1211
+ // In JavaScript, just return the expression wrapped in parentheses
1212
+ return b.sequence([context.visit(node.expression)]);
1213
+ }
1214
+ return context.next();
1215
+ },
1216
+
1203
1217
  ExportNamedDeclaration(node, context) {
1204
1218
  if (!context.state.to_ts && node.exportKind === 'type') {
1205
1219
  return b.empty;
@@ -1282,8 +1296,34 @@ const visitors = {
1282
1296
  return b.block(statements);
1283
1297
  },
1284
1298
 
1285
- ServerBlock() {
1286
- return b.empty;
1299
+ ServerBlock(node, context) {
1300
+ const exports = node.metadata.exports;
1301
+
1302
+ if (exports.length === 0) {
1303
+ return b.empty;
1304
+ }
1305
+ const file_path = context.state.filename;
1306
+
1307
+ return b.const(
1308
+ '_$_server_$_',
1309
+ b.object(
1310
+ exports.map((name) => {
1311
+ const func_path = file_path + '#' + name;
1312
+ // needs to be a sha256 hash of func_path, to avoid leaking file structure
1313
+ const hash = createHash('sha256').update(func_path).digest('hex').slice(0, 8);
1314
+
1315
+ return b.prop(
1316
+ 'init',
1317
+ b.id(name),
1318
+ b.function(
1319
+ null,
1320
+ [b.rest(b.id('args'))],
1321
+ b.block([b.return(b.call('_$_.rpc', b.literal(hash), b.id('args')))]),
1322
+ ),
1323
+ );
1324
+ }),
1325
+ ),
1326
+ );
1287
1327
  },
1288
1328
 
1289
1329
  Program(node, context) {
@@ -1792,6 +1832,7 @@ export function transform_client(filename, source, analysis, to_ts) {
1792
1832
  scopes: analysis.scopes,
1793
1833
  stylesheets: [],
1794
1834
  to_ts,
1835
+ filename,
1795
1836
  };
1796
1837
 
1797
1838
  const program = /** @type {Program} */ (
@@ -17,6 +17,7 @@ import is_reference from 'is-reference';
17
17
  import { escape } from '../../../../utils/escaping.js';
18
18
  import { is_event_attribute } from '../../../../utils/events.js';
19
19
  import { render_stylesheets } from '../stylesheet.js';
20
+ import { createHash } from 'node:crypto';
20
21
 
21
22
  function add_ripple_internal_import(context) {
22
23
  if (!context.state.to_ts) {
@@ -31,6 +32,10 @@ function transform_children(children, context) {
31
32
  const normalized = normalize_children(children, context);
32
33
 
33
34
  for (const node of normalized) {
35
+ if (node.type === 'BreakStatement') {
36
+ state.init.push(b.break);
37
+ continue;
38
+ }
34
39
  if (
35
40
  node.type === 'VariableDeclaration' ||
36
41
  node.type === 'ExpressionStatement' ||
@@ -132,6 +137,39 @@ const visitors = {
132
137
  return context.next();
133
138
  },
134
139
 
140
+ FunctionDeclaration(node, context) {
141
+ if (!context.state.to_ts) {
142
+ delete node.returnType;
143
+ delete node.typeParameters;
144
+ for (const param of node.params) {
145
+ delete param.typeAnnotation;
146
+ }
147
+ }
148
+ return context.next();
149
+ },
150
+
151
+ FunctionExpression(node, context) {
152
+ if (!context.state.to_ts) {
153
+ delete node.returnType;
154
+ delete node.typeParameters;
155
+ for (const param of node.params) {
156
+ delete param.typeAnnotation;
157
+ }
158
+ }
159
+ return context.next();
160
+ },
161
+
162
+ ArrowFunctionExpression(node, context) {
163
+ if (!context.state.to_ts) {
164
+ delete node.returnType;
165
+ delete node.typeParameters;
166
+ for (const param of node.params) {
167
+ delete param.typeAnnotation;
168
+ }
169
+ }
170
+ return context.next();
171
+ },
172
+
135
173
  TSAsExpression(node, context) {
136
174
  if (!context.state.to_ts) {
137
175
  return context.visit(node.expression);
@@ -139,6 +177,14 @@ const visitors = {
139
177
  return context.next();
140
178
  },
141
179
 
180
+ TSInstantiationExpression(node, context) {
181
+ if (!context.state.to_ts) {
182
+ // In JavaScript, just return the expression wrapped in parentheses
183
+ return b.sequence([context.visit(node.expression)]);
184
+ }
185
+ return context.next();
186
+ },
187
+
142
188
  TSTypeAliasDeclaration(_, context) {
143
189
  if (!context.state.to_ts) {
144
190
  return b.empty;
@@ -157,8 +203,23 @@ const visitors = {
157
203
  if (!context.state.to_ts && node.exportKind === 'type') {
158
204
  return b.empty;
159
205
  }
160
-
161
- return context.next();
206
+ if (!context.state.inside_server_block) {
207
+ return context.next();
208
+ }
209
+ const declaration = node.declaration;
210
+
211
+ if (declaration && declaration.type === 'FunctionDeclaration') {
212
+ return b.stmt(
213
+ b.assignment(
214
+ '=',
215
+ b.member(b.id('_$_server_$_'), b.id(declaration.id.name)),
216
+ context.visit(declaration),
217
+ ),
218
+ );
219
+ } else {
220
+ // TODO
221
+ throw new Error('Not implemented');
222
+ }
162
223
  },
163
224
 
164
225
  VariableDeclaration(node, context) {
@@ -380,15 +441,43 @@ const visitors = {
380
441
  }
381
442
  } else {
382
443
  // Component is imported or dynamic - check .async property at runtime
383
- const conditional_await = b.conditional(
384
- b.member(visit(node.id, state), b.id('async')),
385
- b.await(component_call),
386
- component_call,
444
+ // Use if-statement instead of ternary to avoid parser issues with await in conditionals
445
+ state.init.push(
446
+ b.if(
447
+ b.member(visit(node.id, state), b.id('async')),
448
+ b.block([b.stmt(b.await(component_call))]),
449
+ b.block([b.stmt(component_call)]),
450
+ ),
387
451
  );
388
- state.init.push(b.stmt(conditional_await));
452
+
453
+ // Mark parent component as async since we're using await
454
+ if (state.metadata?.await === false) {
455
+ state.metadata.await = true;
456
+ }
389
457
  }
390
458
  }
391
459
  },
460
+ SwitchStatement(node, context) {
461
+ if (!is_inside_component(context)) {
462
+ return context.next();
463
+ }
464
+ const cases = [];
465
+ for (const switch_case of node.cases) {
466
+ const consequent_scope =
467
+ context.state.scopes.get(switch_case.consequent) || context.state.scope;
468
+ const consequent = b.block(
469
+ transform_body(switch_case.consequent, {
470
+ ...context,
471
+ state: { ...context.state, scope: consequent_scope },
472
+ }),
473
+ );
474
+ cases.push(
475
+ b.switch_case(switch_case.test ? context.visit(switch_case.test) : null, consequent.body),
476
+ );
477
+ }
478
+ context.state.init.push(b.switch(context.visit(node.discriminant), cases));
479
+ },
480
+
392
481
  ForOfStatement(node, context) {
393
482
  if (!is_inside_component(context)) {
394
483
  context.next();
@@ -440,6 +529,10 @@ const visitors = {
440
529
  }
441
530
  },
442
531
 
532
+ ServerIdentifier(node, context) {
533
+ return b.id('_$_server_$_');
534
+ },
535
+
443
536
  ImportDeclaration(node, context) {
444
537
  if (!context.state.to_ts && node.importKind === 'type') {
445
538
  return b.empty;
@@ -599,7 +692,38 @@ const visitors = {
599
692
  },
600
693
 
601
694
  ServerBlock(node, context) {
602
- return context.visit(node.body);
695
+ const exports = node.metadata.exports;
696
+
697
+ if (exports.length === 0) {
698
+ return context.visit(node.body);
699
+ }
700
+ const file_path = context.state.filename;
701
+ const block = context.visit(node.body, { ...context.state, inside_server_block: true });
702
+ const rpc_modules = globalThis.rpc_modules;
703
+
704
+ if (rpc_modules) {
705
+ for (const name of exports) {
706
+ const func_path = file_path + '#' + name;
707
+ // needs to be a sha256 hash of func_path, to avoid leaking file structure
708
+ const hash = createHash('sha256').update(func_path).digest('hex').slice(0, 8);
709
+ rpc_modules.set(hash, [file_path, name]);
710
+ }
711
+ }
712
+
713
+ return b.export(
714
+ b.const(
715
+ '_$_server_$_',
716
+ b.call(
717
+ b.thunk(
718
+ b.block([
719
+ b.var('_$_server_$_', b.object([])),
720
+ ...block.body,
721
+ b.return(b.id('_$_server_$_')),
722
+ ]),
723
+ ),
724
+ ),
725
+ ),
726
+ );
603
727
  },
604
728
  };
605
729
 
@@ -614,6 +738,8 @@ export function transform_server(filename, source, analysis) {
614
738
  scopes: analysis.scopes,
615
739
  stylesheets: [],
616
740
  component_metadata,
741
+ inside_server_block: false,
742
+ filename,
617
743
  };
618
744
 
619
745
  const program = /** @type {ESTree.Program} */ (
@@ -169,6 +169,14 @@ export function create_scopes(ast, root, parent) {
169
169
  next({ scope });
170
170
  },
171
171
 
172
+ ServerBlock(node, { state, next }) {
173
+ const scope = state.scope.child();
174
+ scope.server_block = true;
175
+ scopes.set(node, scope);
176
+
177
+ next({ scope });
178
+ },
179
+
172
180
  FunctionExpression(node, { state, next }) {
173
181
  const scope = state.scope.child();
174
182
  scopes.set(node, scope);
@@ -327,6 +335,12 @@ export class Scope {
327
335
  */
328
336
  tracing = null;
329
337
 
338
+ /**
339
+ * Is this scope a top-level server block scope
340
+ * @type {boolean}
341
+ */
342
+ server_block = false;
343
+
330
344
  /**
331
345
  *
332
346
  * @param {ScopeRoot} root
@@ -353,7 +367,7 @@ export class Scope {
353
367
  return this.parent.declare(node, kind, declaration_kind);
354
368
  }
355
369
 
356
- if (declaration_kind === 'import') {
370
+ if (declaration_kind === 'import' && !this.parent.server_block) {
357
371
  return this.parent.declare(node, kind, declaration_kind, initial);
358
372
  }
359
373
  }
@@ -151,7 +151,7 @@ export type DeclarationKind =
151
151
  /**
152
152
  * Binding kinds
153
153
  */
154
- export type BindingKind = 'normal' | 'each' | 'rest_prop' | 'prop' | 'prop_fallback';
154
+ export type BindingKind = 'normal' | 'for_pattern' | 'rest_prop' | 'prop' | 'prop_fallback';
155
155
 
156
156
  /**
157
157
  * A variable binding in a scope
@@ -299,4 +299,4 @@ export interface DelegatedEventResult {
299
299
  hoisted: boolean;
300
300
  /** The hoisted function */
301
301
  function?: FunctionExpression | FunctionDeclaration | ArrowFunctionExpression;
302
- }
302
+ }
@@ -316,6 +316,8 @@ function get_hoisted_params(node, context) {
316
316
  if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
317
317
  if (binding.kind === 'prop') {
318
318
  push_unique(b.id('__props'));
319
+ } else if (binding.kind === 'for_pattern') {
320
+ push_unique(binding.metadata.pattern);
319
321
  } else if (binding.kind === 'prop_fallback') {
320
322
  push_unique(b.id(binding.node.name));
321
323
  } else if (
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { branch, destroy_block, render } from './blocks.js';
4
4
  import { COMPOSITE_BLOCK } from './constants.js';
5
+ import { apply_element_spread } from './render';
5
6
  import { active_block } from './runtime.js';
6
7
 
7
8
  /**
8
- * @param {() => (anchor: Node, props: Record<string, any>, block: Block | null) => void} get_component
9
+ * @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
10
+ * @param {() => ComponentFunction | keyof HTMLElementTagNameMap} get_component
9
11
  * @param {Node} node
10
12
  * @param {Record<string, any>} props
11
13
  * @returns {void}
@@ -23,9 +25,39 @@ export function composite(get_component, node, props) {
23
25
  b = null;
24
26
  }
25
27
 
26
- b = branch(() => {
27
- var block = active_block;
28
- component(anchor, props, block);
29
- });
28
+ if (typeof component === 'function') {
29
+ // Handle as regular component
30
+ b = branch(() => {
31
+ var block = active_block;
32
+ /** @type {ComponentFunction} */ (component)(anchor, props, block);
33
+ });
34
+ } else {
35
+ // Custom element
36
+ b = branch(() => {
37
+ var block = /** @type {Block} */ (active_block);
38
+
39
+ var element = document.createElement(
40
+ /** @type {keyof HTMLElementTagNameMap} */ (component),
41
+ );
42
+ /** @type {ChildNode} */ (anchor).before(element);
43
+
44
+ if (block.s === null) {
45
+ block.s = {
46
+ start: element,
47
+ end: element,
48
+ };
49
+ }
50
+
51
+ const spread_fn = apply_element_spread(element, () => props || {});
52
+ spread_fn();
53
+
54
+ if (typeof props?.children === 'function') {
55
+ var child_anchor = document.createComment('');
56
+ element.appendChild(child_anchor);
57
+
58
+ props?.children?.(child_anchor, {}, block);
59
+ }
60
+ });
61
+ }
30
62
  }, COMPOSITE_BLOCK);
31
63
  }