ripple 0.2.102 → 0.2.104
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 +2 -2
- package/src/compiler/phases/1-parse/index.js +177 -6
- package/src/compiler/phases/2-analyze/index.js +18 -3
- package/src/compiler/phases/3-transform/client/index.js +23 -17
- package/src/compiler/phases/3-transform/segments.js +531 -12
- package/src/compiler/phases/3-transform/server/index.js +237 -19
- package/src/runtime/index-client.js +2 -0
- package/src/runtime/internal/client/css.js +68 -0
- package/src/runtime/internal/server/css-registry.js +35 -0
- package/src/runtime/internal/server/index.js +32 -17
- package/src/server/index.js +2 -1
- package/tests/client/__snapshots__/computed-properties.test.ripple.snap +49 -0
- package/tests/client/composite.test.ripple +78 -75
- package/tests/client/computed-properties.test.ripple +64 -0
- package/tests/client/typescript-generics.test.ripple +140 -29
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.
|
|
6
|
+
"version": "0.2.104",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
67
|
+
"@sveltejs/acorn-typescript": "^1.0.6",
|
|
67
68
|
"acorn": "^8.15.0",
|
|
68
|
-
"acorn-typescript": "^1.4.13",
|
|
69
69
|
"clsx": "^2.1.1",
|
|
70
70
|
"esrap": "^2.1.0",
|
|
71
71
|
"is-reference": "^3.0.3",
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* } from '#compiler' */
|
|
6
6
|
|
|
7
7
|
import * as acorn from 'acorn';
|
|
8
|
-
import { tsPlugin } from 'acorn-typescript';
|
|
8
|
+
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
9
9
|
import { parse_style } from './style.js';
|
|
10
10
|
import { walk } from 'zimmerframe';
|
|
11
11
|
import { regex_newline_characters } from '../../../utils/patterns.js';
|
|
12
12
|
|
|
13
|
-
const parser = acorn.Parser.extend(tsPlugin({
|
|
13
|
+
const parser = acorn.Parser.extend(tsPlugin({ jsx: true }), RipplePlugin());
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Convert JSX node types to regular JavaScript node types
|
|
@@ -67,7 +67,51 @@ function RipplePlugin(config) {
|
|
|
67
67
|
getTokenFromCode(code) {
|
|
68
68
|
if (code === 60) {
|
|
69
69
|
// < character
|
|
70
|
-
|
|
70
|
+
const inComponent = this.#path.findLast((n) => n.type === 'Component');
|
|
71
|
+
|
|
72
|
+
// Check if this could be TypeScript generics instead of JSX
|
|
73
|
+
// TypeScript generics appear after: identifiers, closing parens, 'new' keyword
|
|
74
|
+
// For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
|
|
75
|
+
// This check applies everywhere, not just inside components
|
|
76
|
+
|
|
77
|
+
// Look back to see what precedes the <
|
|
78
|
+
let lookback = this.pos - 1;
|
|
79
|
+
|
|
80
|
+
// Skip whitespace backwards
|
|
81
|
+
while (lookback >= 0) {
|
|
82
|
+
const ch = this.input.charCodeAt(lookback);
|
|
83
|
+
if (ch !== 32 && ch !== 9) break; // not space or tab
|
|
84
|
+
lookback--;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check what character/token precedes the <
|
|
88
|
+
if (lookback >= 0) {
|
|
89
|
+
const prevChar = this.input.charCodeAt(lookback);
|
|
90
|
+
|
|
91
|
+
// If preceded by identifier character (letter, digit, _, $) or closing paren,
|
|
92
|
+
// this is likely TypeScript generics, not JSX
|
|
93
|
+
const isIdentifierChar =
|
|
94
|
+
(prevChar >= 65 && prevChar <= 90) || // A-Z
|
|
95
|
+
(prevChar >= 97 && prevChar <= 122) || // a-z
|
|
96
|
+
(prevChar >= 48 && prevChar <= 57) || // 0-9
|
|
97
|
+
prevChar === 95 || // _
|
|
98
|
+
prevChar === 36 || // $
|
|
99
|
+
prevChar === 41; // )
|
|
100
|
+
|
|
101
|
+
if (isIdentifierChar) {
|
|
102
|
+
return super.getTokenFromCode(code);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (inComponent) {
|
|
107
|
+
// Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
|
|
108
|
+
// At component top-level (scopeStack.length <= 4), apply JSX detection logic
|
|
109
|
+
if (this.scopeStack.length >= 5) {
|
|
110
|
+
// Inside function - treat as TypeScript generic, not JSX
|
|
111
|
+
++this.pos;
|
|
112
|
+
return this.finishToken(tt.relational, '<');
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
// Check if everything before this position on the current line is whitespace
|
|
72
116
|
let lineStart = this.pos - 1;
|
|
73
117
|
while (
|
|
@@ -214,6 +258,88 @@ function RipplePlugin(config) {
|
|
|
214
258
|
return node;
|
|
215
259
|
}
|
|
216
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
|
|
263
|
+
* @param {any} base - The base expression
|
|
264
|
+
* @param {number} startPos - Start position
|
|
265
|
+
* @param {any} startLoc - Start location
|
|
266
|
+
* @param {boolean} noCalls - Whether calls are disallowed
|
|
267
|
+
* @param {any} maybeAsyncArrow - Optional async arrow flag
|
|
268
|
+
* @param {any} optionalChained - Optional chaining flag
|
|
269
|
+
* @param {any} forInit - For-init flag
|
|
270
|
+
* @returns {any} Parsed subscript expression
|
|
271
|
+
*/
|
|
272
|
+
parseSubscripts(
|
|
273
|
+
base,
|
|
274
|
+
startPos,
|
|
275
|
+
startLoc,
|
|
276
|
+
noCalls,
|
|
277
|
+
maybeAsyncArrow,
|
|
278
|
+
optionalChained,
|
|
279
|
+
forInit,
|
|
280
|
+
) {
|
|
281
|
+
// Check for `.@[` pattern for reactive computed member access
|
|
282
|
+
const isDotOrOptional = this.type === tt.dot || this.type === tt.questionDot;
|
|
283
|
+
|
|
284
|
+
if (isDotOrOptional) {
|
|
285
|
+
// Check the next two characters without consuming tokens
|
|
286
|
+
// this.pos currently points AFTER the dot token
|
|
287
|
+
const nextChar = this.input.charCodeAt(this.pos);
|
|
288
|
+
const charAfter = this.input.charCodeAt(this.pos + 1);
|
|
289
|
+
|
|
290
|
+
// Check for @[ pattern (@ = 64, [ = 91)
|
|
291
|
+
if (nextChar === 64 && charAfter === 91) {
|
|
292
|
+
const node = this.startNodeAt(startPos, startLoc);
|
|
293
|
+
node.object = base;
|
|
294
|
+
node.computed = true;
|
|
295
|
+
node.optional = this.type === tt.questionDot;
|
|
296
|
+
node.tracked = true;
|
|
297
|
+
|
|
298
|
+
// Consume the dot/questionDot token
|
|
299
|
+
this.next();
|
|
300
|
+
|
|
301
|
+
// Manually skip the @ character
|
|
302
|
+
this.pos += 1;
|
|
303
|
+
|
|
304
|
+
// Now call finishToken to properly consume the [ bracket
|
|
305
|
+
this.finishToken(tt.bracketL);
|
|
306
|
+
|
|
307
|
+
// Now we're positioned correctly to parse the expression
|
|
308
|
+
this.next(); // Move to first token inside brackets
|
|
309
|
+
|
|
310
|
+
// Parse the expression inside brackets
|
|
311
|
+
node.property = this.parseExpression();
|
|
312
|
+
|
|
313
|
+
// Expect closing bracket
|
|
314
|
+
this.expect(tt.bracketR);
|
|
315
|
+
|
|
316
|
+
// Finish this MemberExpression node
|
|
317
|
+
base = this.finishNode(node, 'MemberExpression');
|
|
318
|
+
|
|
319
|
+
// Recursively handle any further subscripts (chaining)
|
|
320
|
+
return this.parseSubscripts(
|
|
321
|
+
base,
|
|
322
|
+
startPos,
|
|
323
|
+
startLoc,
|
|
324
|
+
noCalls,
|
|
325
|
+
maybeAsyncArrow,
|
|
326
|
+
optionalChained,
|
|
327
|
+
forInit,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Fall back to default parseSubscripts implementation
|
|
333
|
+
return super.parseSubscripts(
|
|
334
|
+
base,
|
|
335
|
+
startPos,
|
|
336
|
+
startLoc,
|
|
337
|
+
noCalls,
|
|
338
|
+
maybeAsyncArrow,
|
|
339
|
+
optionalChained,
|
|
340
|
+
forInit,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
217
343
|
/**
|
|
218
344
|
* Parse expression atom - handles TrackedArray and TrackedObject literals
|
|
219
345
|
* @param {any} [refDestructuringErrors]
|
|
@@ -1230,7 +1356,7 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1230
1356
|
|
|
1231
1357
|
comments = comments
|
|
1232
1358
|
.filter((comment) => comment.start >= index)
|
|
1233
|
-
.map(({ type, value, start, end }) => ({ type, value, start, end }));
|
|
1359
|
+
.map(({ type, value, start, end, loc }) => ({ type, value, start, end, loc }));
|
|
1234
1360
|
|
|
1235
1361
|
walk(ast, null, {
|
|
1236
1362
|
_(node, { next, path }) {
|
|
@@ -1301,11 +1427,56 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1301
1427
|
export function parse(source) {
|
|
1302
1428
|
/** @type {CommentWithLocation[]} */
|
|
1303
1429
|
const comments = [];
|
|
1304
|
-
|
|
1430
|
+
|
|
1431
|
+
// Preprocess step 1: Add trailing commas to single-parameter generics followed by (
|
|
1432
|
+
// This is a workaround for @sveltejs/acorn-typescript limitations with JSX enabled
|
|
1433
|
+
let preprocessedSource = source;
|
|
1434
|
+
let sourceChanged = false;
|
|
1435
|
+
|
|
1436
|
+
preprocessedSource = source.replace(/(<\s*[A-Z][a-zA-Z0-9_$]*\s*)>\s*\(/g, (_, generic) => {
|
|
1437
|
+
sourceChanged = true;
|
|
1438
|
+
// Add trailing comma to disambiguate from JSX
|
|
1439
|
+
return `${generic},>(`;
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
// Preprocess step 2: Convert generic method shorthand in object literals to function property syntax
|
|
1443
|
+
// Transform `method<T,>(...): ReturnType { body }` to `method: function<T,>(...): ReturnType { body }`
|
|
1444
|
+
// Note: This only applies to object literal methods, not class methods
|
|
1445
|
+
// The trailing comma was already added by step 1
|
|
1446
|
+
preprocessedSource = preprocessedSource.replace(
|
|
1447
|
+
/(\w+)(<[A-Z][a-zA-Z0-9_$,\s]*>)\s*\(([^)]*)\)(\s*:\s*[^{]+)?(\s*\{)/g,
|
|
1448
|
+
(match, methodName, generics, params, returnType, brace, offset) => {
|
|
1449
|
+
// Look backward to determine context
|
|
1450
|
+
let checkPos = offset - 1;
|
|
1451
|
+
while (checkPos >= 0 && /\s/.test(preprocessedSource[checkPos])) checkPos--;
|
|
1452
|
+
const prevChar = preprocessedSource[checkPos];
|
|
1453
|
+
|
|
1454
|
+
// Check if we're inside a class
|
|
1455
|
+
const before = preprocessedSource.substring(Math.max(0, offset - 500), offset);
|
|
1456
|
+
const classMatch = before.match(/\bclass\s+\w+[^{]*\{[^}]*$/);
|
|
1457
|
+
|
|
1458
|
+
// Only transform if we're in an object literal context AND not inside a class
|
|
1459
|
+
if ((prevChar === '{' || prevChar === ',') && !classMatch) {
|
|
1460
|
+
sourceChanged = true;
|
|
1461
|
+
// This is object literal method shorthand - convert to function property
|
|
1462
|
+
// Add trailing comma if not already present
|
|
1463
|
+
const fixedGenerics = generics.includes(',') ? generics : generics.replace('>', ',>');
|
|
1464
|
+
return `${methodName}: function${fixedGenerics}(${params})${returnType || ''}${brace}`;
|
|
1465
|
+
}
|
|
1466
|
+
return match;
|
|
1467
|
+
},
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
// Only mark as preprocessed if we actually changed something
|
|
1471
|
+
if (!sourceChanged) {
|
|
1472
|
+
preprocessedSource = source;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const { onComment, add_comments } = get_comment_handlers(preprocessedSource, comments);
|
|
1305
1476
|
let ast;
|
|
1306
1477
|
|
|
1307
1478
|
try {
|
|
1308
|
-
ast = parser.parse(
|
|
1479
|
+
ast = parser.parse(preprocessedSource, {
|
|
1309
1480
|
sourceType: 'module',
|
|
1310
1481
|
ecmaVersion: 13,
|
|
1311
1482
|
locations: true,
|
|
@@ -106,7 +106,7 @@ const visitors = {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
if (
|
|
109
|
-
is_reference(node, /** @type {Node} */
|
|
109
|
+
is_reference(node, /** @type {Node} */(parent)) &&
|
|
110
110
|
node.tracked &&
|
|
111
111
|
binding?.node !== node
|
|
112
112
|
) {
|
|
@@ -117,7 +117,7 @@ const visitors = {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
if (
|
|
120
|
-
is_reference(node, /** @type {Node} */
|
|
120
|
+
is_reference(node, /** @type {Node} */(parent)) &&
|
|
121
121
|
node.tracked &&
|
|
122
122
|
binding?.node !== node
|
|
123
123
|
) {
|
|
@@ -250,7 +250,10 @@ const visitors = {
|
|
|
250
250
|
}
|
|
251
251
|
const elements = [];
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
// Track metadata for this component
|
|
254
|
+
const metadata = { await: false };
|
|
255
|
+
|
|
256
|
+
context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1, metadata });
|
|
254
257
|
|
|
255
258
|
const css = node.css;
|
|
256
259
|
|
|
@@ -259,6 +262,12 @@ const visitors = {
|
|
|
259
262
|
prune_css(css, node);
|
|
260
263
|
}
|
|
261
264
|
}
|
|
265
|
+
|
|
266
|
+
// Store component metadata in analysis
|
|
267
|
+
context.state.analysis.component_metadata.push({
|
|
268
|
+
id: node.id.name,
|
|
269
|
+
async: metadata.await,
|
|
270
|
+
});
|
|
262
271
|
},
|
|
263
272
|
|
|
264
273
|
ForStatement(node, context) {
|
|
@@ -357,6 +366,11 @@ const visitors = {
|
|
|
357
366
|
}
|
|
358
367
|
|
|
359
368
|
if (node.pending) {
|
|
369
|
+
// Try/pending blocks indicate async operations
|
|
370
|
+
if (context.state.metadata?.await === false) {
|
|
371
|
+
context.state.metadata.await = true;
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
node.metadata = {
|
|
361
375
|
has_template: false,
|
|
362
376
|
};
|
|
@@ -594,6 +608,7 @@ export function analyze(ast, filename) {
|
|
|
594
608
|
ast,
|
|
595
609
|
scope,
|
|
596
610
|
scopes,
|
|
611
|
+
component_metadata: [],
|
|
597
612
|
};
|
|
598
613
|
|
|
599
614
|
walk(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @import {Expression, FunctionExpression} from 'estree' */
|
|
1
|
+
/** @import {Expression, FunctionExpression, Pattern} from 'estree' */
|
|
2
2
|
|
|
3
3
|
import { walk } from 'zimmerframe';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -293,7 +293,6 @@ const visitors = {
|
|
|
293
293
|
|
|
294
294
|
NewExpression(node, context) {
|
|
295
295
|
const callee = node.callee;
|
|
296
|
-
const parent = context.path.at(-1);
|
|
297
296
|
|
|
298
297
|
if (context.state.metadata?.tracking === false) {
|
|
299
298
|
context.state.metadata.tracking = true;
|
|
@@ -305,18 +304,22 @@ const visitors = {
|
|
|
305
304
|
is_inside_call_expression(context) ||
|
|
306
305
|
is_value_static(node)
|
|
307
306
|
) {
|
|
307
|
+
if (!context.state.to_ts) {
|
|
308
|
+
delete node.typeArguments;
|
|
309
|
+
}
|
|
308
310
|
return context.next();
|
|
309
311
|
}
|
|
310
312
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
313
|
+
const new_node = {
|
|
314
|
+
...node,
|
|
315
|
+
callee: context.visit(callee),
|
|
316
|
+
arguments: node.arguments.map((arg) => context.visit(arg)),
|
|
317
|
+
};
|
|
318
|
+
if (!context.state.to_ts) {
|
|
319
|
+
delete new_node.typeArguments;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return b.call('_$_.with_scope', b.id('__block'), b.thunk(new_node));
|
|
320
323
|
},
|
|
321
324
|
|
|
322
325
|
TrackedArrayExpression(node, context) {
|
|
@@ -366,7 +369,7 @@ const visitors = {
|
|
|
366
369
|
context.state.metadata.tracking = true;
|
|
367
370
|
}
|
|
368
371
|
|
|
369
|
-
if (node.property.type === 'Identifier' && node.property.tracked) {
|
|
372
|
+
if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
|
|
370
373
|
add_ripple_internal_import(context);
|
|
371
374
|
|
|
372
375
|
return b.call(
|
|
@@ -918,8 +921,7 @@ const visitors = {
|
|
|
918
921
|
|
|
919
922
|
if (
|
|
920
923
|
left.type === 'MemberExpression' &&
|
|
921
|
-
left.property.type === 'Identifier' &&
|
|
922
|
-
left.property.tracked
|
|
924
|
+
(left.tracked || (left.property.type === 'Identifier' && left.property.tracked))
|
|
923
925
|
) {
|
|
924
926
|
add_ripple_internal_import(context);
|
|
925
927
|
const operator = node.operator;
|
|
@@ -976,8 +978,7 @@ const visitors = {
|
|
|
976
978
|
|
|
977
979
|
if (
|
|
978
980
|
argument.type === 'MemberExpression' &&
|
|
979
|
-
argument.property.type === 'Identifier' &&
|
|
980
|
-
argument.property.tracked
|
|
981
|
+
(argument.tracked || (argument.property.type === 'Identifier' && argument.property.tracked))
|
|
981
982
|
) {
|
|
982
983
|
add_ripple_internal_import(context);
|
|
983
984
|
context.state.metadata.tracking = true;
|
|
@@ -1052,7 +1053,9 @@ const visitors = {
|
|
|
1052
1053
|
),
|
|
1053
1054
|
),
|
|
1054
1055
|
b.literal(flags),
|
|
1055
|
-
key != null
|
|
1056
|
+
key != null
|
|
1057
|
+
? b.arrow(index ? [pattern, index] : [pattern], context.visit(key))
|
|
1058
|
+
: undefined,
|
|
1056
1059
|
),
|
|
1057
1060
|
),
|
|
1058
1061
|
);
|
|
@@ -1401,6 +1404,9 @@ function transform_ts_child(node, context) {
|
|
|
1401
1404
|
...context,
|
|
1402
1405
|
state: { ...context.state, scope: body_scope },
|
|
1403
1406
|
});
|
|
1407
|
+
if (node.key) {
|
|
1408
|
+
block_body.unshift(b.stmt(visit(node.key)));
|
|
1409
|
+
}
|
|
1404
1410
|
if (node.index) {
|
|
1405
1411
|
block_body.unshift(b.let(visit(node.index), b.literal(0)));
|
|
1406
1412
|
}
|