ripple 0.2.90 → 0.2.92
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 +1 -1
- package/src/compiler/index.js +14 -8
- package/src/compiler/phases/1-parse/index.js +74 -20
- package/src/compiler/phases/3-transform/client/index.js +101 -49
- package/src/compiler/phases/3-transform/segments.js +248 -34
- package/src/compiler/scope.js +478 -404
- package/src/compiler/types/index.d.ts +299 -3
- package/src/compiler/utils.js +173 -30
- package/src/runtime/date.js +73 -0
- package/src/runtime/index-client.js +24 -21
- package/src/runtime/internal/client/html.js +18 -8
- package/src/runtime/internal/client/index.js +1 -0
- package/src/runtime/internal/client/portal.js +55 -32
- package/src/runtime/internal/client/render.js +31 -1
- package/src/runtime/internal/client/runtime.js +53 -22
- package/src/utils/normalize_css_property_name.js +23 -0
- package/tests/client/basic.test.ripple +207 -1
- package/tests/client/compiler.test.ripple +95 -1
- package/tests/client/date.test.ripple +392 -0
- package/tests/client/html.test.ripple +29 -1
- package/tests/client/portal.test.ripple +167 -0
- package/types/index.d.ts +37 -28
|
@@ -7,14 +7,100 @@ export const mapping_data = {
|
|
|
7
7
|
navigation: true,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Helper to find a meaningful token boundary by looking for word boundaries,
|
|
12
|
+
* punctuation, or whitespace
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @param {number} start
|
|
15
|
+
* @param {number} direction
|
|
16
|
+
*/
|
|
17
|
+
function findTokenBoundary(text, start, direction = 1) {
|
|
18
|
+
if (start < 0 || start >= text.length) return start;
|
|
19
|
+
|
|
20
|
+
let pos = start;
|
|
21
|
+
/** @param {string} c */
|
|
22
|
+
const isAlphaNum = (c) => /[a-zA-Z0-9_$]/.test(c);
|
|
23
|
+
|
|
24
|
+
// If we're at whitespace or punctuation, find the next meaningful character
|
|
25
|
+
while (pos >= 0 && pos < text.length && /\s/.test(text[pos])) {
|
|
26
|
+
pos += direction;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (pos < 0 || pos >= text.length) return start;
|
|
30
|
+
|
|
31
|
+
// If we're in the middle of a word/identifier, find the boundary
|
|
32
|
+
if (isAlphaNum(text[pos])) {
|
|
33
|
+
if (direction > 0) {
|
|
34
|
+
while (pos < text.length && isAlphaNum(text[pos])) pos++;
|
|
35
|
+
} else {
|
|
36
|
+
while (pos >= 0 && isAlphaNum(text[pos])) pos--;
|
|
37
|
+
pos++; // Adjust back to start of token
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
// For punctuation, just move one character in the given direction
|
|
41
|
+
pos += direction;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Math.max(0, Math.min(text.length, pos));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if source and generated content are meaningfully similar
|
|
49
|
+
* @param {string} sourceContent
|
|
50
|
+
* @param {string} generatedContent
|
|
51
|
+
*/
|
|
52
|
+
function isValidMapping(sourceContent, generatedContent) {
|
|
53
|
+
// Remove whitespace for comparison
|
|
54
|
+
const cleanSource = sourceContent.replace(/\s+/g, '');
|
|
55
|
+
const cleanGenerated = generatedContent.replace(/\s+/g, '');
|
|
56
|
+
|
|
57
|
+
// If either is empty, skip
|
|
58
|
+
if (!cleanSource || !cleanGenerated) return false;
|
|
59
|
+
|
|
60
|
+
// Skip obvious template transformations that don't make sense to map
|
|
61
|
+
const templateTransforms = [
|
|
62
|
+
/^\{.*\}$/, // Curly brace expressions
|
|
63
|
+
/^<.*>$/, // HTML tags
|
|
64
|
+
/^\(\(\)\s*=>\s*\{$/, // Generated function wrappers
|
|
65
|
+
/^\}\)\(\)\}$/, // Generated function closures
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const transform of templateTransforms) {
|
|
69
|
+
if (transform.test(cleanSource) || transform.test(cleanGenerated)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if content is similar (exact match, or generated contains source)
|
|
75
|
+
if (cleanSource === cleanGenerated) return true;
|
|
76
|
+
if (cleanGenerated.includes(cleanSource)) return true;
|
|
77
|
+
if (cleanSource.includes(cleanGenerated) && cleanGenerated.length > 2) return true;
|
|
78
|
+
|
|
79
|
+
// Special handling for ref callback parameters and types in createRefKey context
|
|
80
|
+
if (sourceContent.match(/\w+:\s*\w+/) && generatedContent.match(/\w+:\s*\w+/)) {
|
|
81
|
+
// This looks like a parameter with type annotation, allow mapping
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Allow mapping of identifiers that appear in both source and generated
|
|
86
|
+
if (sourceContent.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) &&
|
|
87
|
+
generatedContent.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) &&
|
|
88
|
+
sourceContent === generatedContent) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
10
95
|
/**
|
|
11
96
|
* Convert esrap SourceMap to Volar mappings
|
|
12
|
-
* @param {
|
|
97
|
+
* @param {{ mappings: string }} source_map
|
|
13
98
|
* @param {string} source
|
|
14
99
|
* @param {string} generated_code
|
|
15
100
|
* @returns {object}
|
|
16
101
|
*/
|
|
17
102
|
export function convert_source_map_to_mappings(source_map, source, generated_code) {
|
|
103
|
+
/** @type {Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}>} */
|
|
18
104
|
const mappings = [];
|
|
19
105
|
|
|
20
106
|
// Decode the VLQ mappings from esrap
|
|
@@ -22,6 +108,7 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
|
|
|
22
108
|
|
|
23
109
|
let generated_offset = 0;
|
|
24
110
|
const generated_lines = generated_code.split('\n');
|
|
111
|
+
const source_lines = source.split('\n');
|
|
25
112
|
|
|
26
113
|
// Process each line of generated code
|
|
27
114
|
for (let generated_line = 0; generated_line < generated_lines.length; generated_line++) {
|
|
@@ -38,7 +125,6 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
|
|
|
38
125
|
}
|
|
39
126
|
|
|
40
127
|
// Calculate source offset
|
|
41
|
-
const source_lines = source.split('\n');
|
|
42
128
|
let source_offset = 0;
|
|
43
129
|
for (let i = 0; i < Math.min(source_line, source_lines.length - 1); i++) {
|
|
44
130
|
source_offset += source_lines[i].length + 1; // +1 for newline
|
|
@@ -48,41 +134,166 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
|
|
|
48
134
|
// Calculate generated offset
|
|
49
135
|
const current_generated_offset = generated_offset + generated_column;
|
|
50
136
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
137
|
+
// Find meaningful token boundaries for source content
|
|
138
|
+
const source_token_end = findTokenBoundary(source, source_offset, 1);
|
|
139
|
+
const source_token_start = findTokenBoundary(source, source_offset, -1);
|
|
140
|
+
|
|
141
|
+
// Find meaningful token boundaries for generated content
|
|
142
|
+
const generated_token_end = findTokenBoundary(generated_code, current_generated_offset, 1);
|
|
143
|
+
const generated_token_start = findTokenBoundary(generated_code, current_generated_offset, -1);
|
|
144
|
+
|
|
145
|
+
// Extract potential source content (prefer forward boundary but try both directions)
|
|
146
|
+
let best_source_content = source.substring(source_offset, source_token_end);
|
|
147
|
+
let best_generated_content = generated_code.substring(current_generated_offset, generated_token_end);
|
|
148
|
+
|
|
149
|
+
// Try different segment boundaries to find the best match
|
|
150
|
+
const candidates = [
|
|
151
|
+
// Forward boundaries
|
|
152
|
+
{
|
|
153
|
+
source: source.substring(source_offset, source_token_end),
|
|
154
|
+
generated: generated_code.substring(current_generated_offset, generated_token_end)
|
|
155
|
+
},
|
|
156
|
+
// Backward boundaries
|
|
157
|
+
{
|
|
158
|
+
source: source.substring(source_token_start, source_offset + 1),
|
|
159
|
+
generated: generated_code.substring(generated_token_start, current_generated_offset + 1)
|
|
160
|
+
},
|
|
161
|
+
// Single character
|
|
162
|
+
{
|
|
163
|
+
source: source.charAt(source_offset),
|
|
164
|
+
generated: generated_code.charAt(current_generated_offset)
|
|
165
|
+
},
|
|
166
|
+
// Try to find exact matches in nearby content
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// Look for the best candidate match
|
|
170
|
+
let best_match = null;
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
if (isValidMapping(candidate.source, candidate.generated)) {
|
|
173
|
+
best_match = candidate;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
67
176
|
}
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
177
|
+
|
|
178
|
+
// If no good match found, try extracting identifiers/keywords
|
|
179
|
+
if (!best_match) {
|
|
180
|
+
const sourceIdMatch = source.substring(source_offset).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
|
|
181
|
+
const generatedIdMatch = generated_code.substring(current_generated_offset).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
|
|
182
|
+
|
|
183
|
+
if (sourceIdMatch && generatedIdMatch && sourceIdMatch[0] === generatedIdMatch[0]) {
|
|
184
|
+
best_match = {
|
|
185
|
+
source: sourceIdMatch[0],
|
|
186
|
+
generated: generatedIdMatch[0]
|
|
187
|
+
};
|
|
77
188
|
}
|
|
78
189
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
190
|
+
|
|
191
|
+
// Special handling for type annotations in ref callbacks
|
|
192
|
+
if (!best_match) {
|
|
193
|
+
// Look for type annotations like "HTMLButtonElement"
|
|
194
|
+
const sourceTypeMatch = source.substring(source_offset).match(/^[A-Z][a-zA-Z0-9]*(?:Element|Type|Interface)?/);
|
|
195
|
+
const generatedTypeMatch = generated_code.substring(current_generated_offset).match(/^[A-Z][a-zA-Z0-9]*(?:Element|Type|Interface)?/);
|
|
196
|
+
|
|
197
|
+
if (sourceTypeMatch && generatedTypeMatch && sourceTypeMatch[0] === generatedTypeMatch[0]) {
|
|
198
|
+
best_match = {
|
|
199
|
+
source: sourceTypeMatch[0],
|
|
200
|
+
generated: generatedTypeMatch[0]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle special cases for Ripple keywords that might not have generated equivalents
|
|
206
|
+
if (!best_match || best_match.source.length === 0) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Special handling for Ripple-specific syntax that may be omitted in generated code
|
|
211
|
+
const sourceAtOffset = source.substring(source_offset, source_offset + 10);
|
|
212
|
+
if (sourceAtOffset.includes('index ')) {
|
|
213
|
+
// For the 'index' keyword, create a mapping even if there's no generated equivalent
|
|
214
|
+
const indexMatch = sourceAtOffset.match(/index\s+/);
|
|
215
|
+
if (indexMatch) {
|
|
216
|
+
best_match = {
|
|
217
|
+
source: indexMatch[0].trim(),
|
|
218
|
+
generated: '' // Empty generated content for keywords that are transformed away
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Skip if we still don't have a valid source match
|
|
224
|
+
if (!best_match || best_match.source.length === 0) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle special ref syntax mapping for createRefKey() pattern
|
|
229
|
+
const sourceAtRefOffset = source.substring(Math.max(0, source_offset - 20), source_offset + 20);
|
|
230
|
+
const generatedAtRefOffset = generated_code.substring(Math.max(0, current_generated_offset - 20), current_generated_offset + 20);
|
|
231
|
+
|
|
232
|
+
// Check if we're dealing with ref callback syntax in source and createRefKey in generated
|
|
233
|
+
if (sourceAtRefOffset.includes('{ref ') && generatedAtRefOffset.includes('createRefKey')) {
|
|
234
|
+
// Look for the ref callback pattern in source: {ref (param: Type) => { ... }}
|
|
235
|
+
const refMatch = source.substring(source_offset - 50, source_offset + 50).match(/\{ref\s*\(([^)]+)\)\s*=>/);
|
|
236
|
+
if (refMatch) {
|
|
237
|
+
const paramMatch = refMatch[1].match(/(\w+):\s*(\w+)/);
|
|
238
|
+
if (paramMatch) {
|
|
239
|
+
const paramName = paramMatch[1];
|
|
240
|
+
const typeName = paramMatch[2];
|
|
241
|
+
|
|
242
|
+
// Map the parameter name to the generated callback parameter
|
|
243
|
+
if (best_match.source === paramName || best_match.source.includes(paramName)) {
|
|
244
|
+
// This is a ref callback parameter, allow the mapping
|
|
245
|
+
}
|
|
246
|
+
// Map the type annotation
|
|
247
|
+
else if (best_match.source === typeName || best_match.source.includes(typeName)) {
|
|
248
|
+
// This is a type annotation in ref callback, allow the mapping
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Skip mappings for complex RefAttribute syntax to avoid overlapping sourcemaps,
|
|
255
|
+
// but allow mappings that are part of the createRefKey pattern
|
|
256
|
+
if (best_match.source.includes('{ref ') && best_match.source.length > 10 &&
|
|
257
|
+
!generatedAtRefOffset.includes('createRefKey')) {
|
|
258
|
+
// Skip complex ref expressions like '{ref (node) => { ... }}' only if not using createRefKey
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Allow simple 'ref' keyword mappings for IntelliSense
|
|
263
|
+
if (best_match.source.trim() === 'ref' && best_match.generated.length === 0) {
|
|
264
|
+
// This is just the ref keyword, allow it for syntax support
|
|
265
|
+
// but map it to current position since there's no generated equivalent
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Calculate actual offsets and lengths for the best match
|
|
269
|
+
let actual_source_offset, actual_generated_offset;
|
|
270
|
+
|
|
271
|
+
if (best_match.generated.length > 0) {
|
|
272
|
+
actual_source_offset = source.indexOf(best_match.source, source_offset - best_match.source.length);
|
|
273
|
+
actual_generated_offset = generated_code.indexOf(best_match.generated, current_generated_offset - best_match.generated.length);
|
|
274
|
+
} else {
|
|
275
|
+
// For keywords with no generated equivalent, use the exact source position
|
|
276
|
+
actual_source_offset = source_offset;
|
|
277
|
+
actual_generated_offset = current_generated_offset; // Map to current position in generated code
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Use the match we found, but fall back to original positions if indexOf fails
|
|
281
|
+
const final_source_offset = actual_source_offset !== -1 ? actual_source_offset : source_offset;
|
|
282
|
+
const final_generated_offset = actual_generated_offset !== -1 ? actual_generated_offset : current_generated_offset; // Avoid duplicate mappings by checking if we already have this exact mapping
|
|
283
|
+
const isDuplicate = mappings.some(existing =>
|
|
284
|
+
existing.sourceOffsets[0] === final_source_offset &&
|
|
285
|
+
existing.generatedOffsets[0] === final_generated_offset &&
|
|
286
|
+
existing.lengths[0] === best_match.source.length
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!isDuplicate) {
|
|
290
|
+
mappings.push({
|
|
291
|
+
sourceOffsets: [final_source_offset],
|
|
292
|
+
generatedOffsets: [final_generated_offset],
|
|
293
|
+
lengths: [best_match.source.length],
|
|
294
|
+
data: mapping_data,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
86
297
|
}
|
|
87
298
|
|
|
88
299
|
// Add line length + 1 for newline (except for last line)
|
|
@@ -92,6 +303,9 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
|
|
|
92
303
|
}
|
|
93
304
|
}
|
|
94
305
|
|
|
306
|
+
// Sort mappings by source offset for better organization
|
|
307
|
+
mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
|
|
308
|
+
|
|
95
309
|
return {
|
|
96
310
|
code: generated_code,
|
|
97
311
|
mappings,
|