ripple 0.2.185 → 0.2.187

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.
@@ -1,22 +1,51 @@
1
1
  /**
2
- @import { CustomMappingData, PluginActionOverrides } from 'ripple/compiler';
3
- @import { DocumentHighlightKind } from 'vscode-languageserver-types';
4
2
  @import * as AST from 'estree';
5
3
  @import * as ESTreeJSX from 'estree-jsx';
6
- @import {MappingData, CodeMapping, VolarMappingsResult} from 'ripple/compiler';
7
- @import {CodeMapping as VolarCodeMapping} from '@volar/language-core';
8
- */
4
+ @import { DocumentHighlightKind } from 'vscode-languageserver-types';
5
+ @import { CodeMapping as VolarCodeMapping } from '@volar/language-core';
6
+ @import { SourceMapMappings } from '@jridgewell/sourcemap-codec';
7
+ @import {
8
+ CustomMappingData,
9
+ PluginActionOverrides,
10
+ MappingData,
11
+ CodeMapping,
12
+ VolarMappingsResult,
13
+ } from 'ripple/compiler';
14
+ @import { PostProcessingChanges } from './client/index.js';
15
+ */
9
16
 
10
17
  /**
11
- * @typedef {{
12
- * start: number,
13
- * end: number,
14
- * content: string
15
- * }} CssSourceRegion
18
+ @typedef {{
19
+ start: number,
20
+ end: number,
21
+ content: string,
22
+ id: string,
23
+ }} CssSourceRegion;
24
+ @typedef {{
25
+ source: string | null | undefined;
26
+ generated: string;
27
+ is_full_import_statement?: boolean;
28
+ loc: AST.SourceLocation;
29
+ end_loc?: AST.SourceLocation;
30
+ metadata?: PluginActionOverrides;
31
+ }} Token;
32
+ @typedef {{
33
+ name: string,
34
+ line: number,
35
+ column: number,
36
+ offset: number,
37
+ length: number,
38
+ sourceOffset: number,
39
+ }} TokenClass
40
+ @typedef {Map<string, AST.Element['metadata']['css']>} CssElementInfo
16
41
  */
17
42
 
18
43
  import { walk } from 'zimmerframe';
19
- import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
44
+ import {
45
+ build_src_to_gen_map,
46
+ get_generated_position,
47
+ offset_to_line_col,
48
+ } from '../../source-map-utils.js';
20
49
 
21
50
  /** @type {VolarCodeMapping['data']} */
22
51
  export const mapping_data = {
@@ -28,6 +57,15 @@ export const mapping_data = {
28
57
  format: false,
29
58
  };
30
59
 
60
+ /**
61
+ * @param {string} [hash]
62
+ * @param {string} [fallback]
63
+ * @returns `style-${hash | fallback}`
64
+ */
65
+ function get_style_region_id(hash, fallback) {
66
+ return `style-${hash || fallback}`;
67
+ }
68
+
31
69
  /**
32
70
  * Converts line/column positions to byte offsets
33
71
  * @param {string} text
@@ -62,47 +100,205 @@ function loc_to_offset(line, column, line_offsets) {
62
100
  /**
63
101
  * Extract CSS source regions from style elements in the AST
64
102
  * @param {AST.Node} ast - The parsed AST
65
- * @param {string} source - Original source code
66
- * @param {number[]} source_line_offsets
67
- * @returns {CssSourceRegion[]}
103
+ * @param {number[]} src_line_offsets
104
+ * @param {{
105
+ * regions: CssSourceRegion[],
106
+ * css_element_info: CssElementInfo,
107
+ * }} param2
108
+ * @returns {void}
68
109
  */
69
- function extractCssSourceRegions(ast, source, source_line_offsets) {
70
- /** @type {CssSourceRegion[]} */
71
- const regions = [];
72
-
110
+ function visit_source_ast(ast, src_line_offsets, { regions, css_element_info }) {
111
+ let region_id = 0;
73
112
  walk(ast, null, {
74
- Element(node) {
113
+ Element(node, context) {
75
114
  // Check if this is a style element with CSS content
76
115
  if (node.id?.name === 'style' && node.css) {
77
116
  const openLoc = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
78
117
  node.openingElement
79
118
  ).loc;
80
- const cssStart = loc_to_offset(openLoc.end.line, openLoc.end.column, source_line_offsets);
119
+ const cssStart = loc_to_offset(openLoc.end.line, openLoc.end.column, src_line_offsets);
81
120
 
82
121
  const closeLoc = /** @type {ESTreeJSX.JSXClosingElement & AST.NodeWithLocation} */ (
83
122
  node.closingElement
84
123
  ).loc;
85
- const cssEnd = loc_to_offset(
86
- closeLoc.start.line,
87
- closeLoc.start.column,
88
- source_line_offsets,
89
- );
124
+ const cssEnd = loc_to_offset(closeLoc.start.line, closeLoc.start.column, src_line_offsets);
90
125
 
91
126
  regions.push({
92
127
  start: cssStart,
93
128
  end: cssEnd,
94
129
  content: node.css,
130
+ id: get_style_region_id(node.metadata.styleScopeHash, `head-${region_id}`),
95
131
  });
96
132
  }
133
+
134
+ context.next();
97
135
  },
98
- });
136
+ Attribute(node, context) {
137
+ const element = context.path?.find((n) => n.type === 'Element');
138
+ if (element?.metadata?.css?.scopedClasses) {
139
+ // we don't need to check is_element_dom_element(node)
140
+ // since scopedClasses are added during pruning only to DOM elements
141
+ const css = element.metadata.css;
142
+ const { line, column } = node.value?.loc?.start ?? {};
99
143
 
100
- return regions;
144
+ if (line === undefined || column === undefined) {
145
+ return;
146
+ }
147
+
148
+ css_element_info.set(`${line}:${column}`, css);
149
+ }
150
+ },
151
+ });
101
152
  }
102
153
 
103
154
  /**
104
- * @import { PostProcessingChanges } from './client/index.js';
155
+ * Extract individual class names and their offsets from class attribute values
156
+ * Handles: "foo bar", { foo: true }, ['foo', { bar: true }], etc.
157
+ *
158
+ * @param {AST.Node} node - The attribute value node
159
+ * @param {ReturnType<typeof build_src_to_gen_map>[0]} src_to_gen_map
160
+ * @param {number[]} gen_line_offsets
161
+ * @param {number[]} src_line_offsets
162
+ * @returns {TokenClass[]}
105
163
  */
164
+ function extract_classes(node, src_to_gen_map, gen_line_offsets, src_line_offsets) {
165
+ /** @type {TokenClass[]} */
166
+ const classes = [];
167
+
168
+ switch (node.type) {
169
+ case 'Literal': {
170
+ // Static: class="foo bar baz"
171
+
172
+ const content = node.raw ?? '';
173
+ let text = content;
174
+ let textOffset = 0;
175
+
176
+ // Remove quotes
177
+ if (
178
+ (content.startsWith(`'`) && content.endsWith(`'`)) ||
179
+ (content.startsWith(`"`) && content.endsWith(`"`)) ||
180
+ (content.startsWith('`') && content.endsWith('`'))
181
+ ) {
182
+ text = content.slice(1, -1);
183
+ textOffset = 1;
184
+ }
185
+
186
+ // Split by whitespace
187
+ const classNames = text.split(/\s+/).filter((c) => c.length > 0);
188
+ const nodeSrcStart = /** @type {AST.Position} */ (node.loc?.start);
189
+
190
+ let currentPos = 0;
191
+ const nodeGenStart = get_generated_position(
192
+ nodeSrcStart.line,
193
+ nodeSrcStart.column,
194
+ src_to_gen_map,
195
+ );
196
+ const offset = loc_to_offset(nodeGenStart.line, nodeGenStart.column, gen_line_offsets);
197
+ const sourceOffset = loc_to_offset(nodeSrcStart.line, nodeSrcStart.column, src_line_offsets);
198
+
199
+ for (const name of classNames) {
200
+ const classStart = text.indexOf(name, currentPos);
201
+ const classOffset = offset + textOffset + classStart;
202
+ const classSourceOffset = sourceOffset + textOffset + classStart;
203
+ const { line, column } = offset_to_line_col(classOffset, gen_line_offsets);
204
+
205
+ classes.push({
206
+ name,
207
+ line,
208
+ column,
209
+ offset: classOffset,
210
+ length: name.length,
211
+ sourceOffset: classSourceOffset,
212
+ });
213
+
214
+ currentPos = classStart + name.length;
215
+ }
216
+ break;
217
+ }
218
+
219
+ case 'ObjectExpression': {
220
+ // Dynamic: class={{ foo: true, bar: @show }}
221
+ for (const prop of node.properties) {
222
+ if (prop.type === 'Property' && prop.key) {
223
+ const key = prop.key;
224
+ if (key.type === 'Identifier' && key.name && key.loc) {
225
+ const nodeSrcStart = /** @type {AST.Position} */ (key.loc?.start);
226
+ const nodeGenStart = get_generated_position(
227
+ nodeSrcStart.line,
228
+ nodeSrcStart.column,
229
+ src_to_gen_map,
230
+ );
231
+ const offset = loc_to_offset(nodeGenStart.line, nodeGenStart.column, gen_line_offsets);
232
+ const sourceOffset = loc_to_offset(
233
+ nodeSrcStart.line,
234
+ nodeSrcStart.column,
235
+ src_line_offsets,
236
+ );
237
+ const { line, column } = offset_to_line_col(offset, gen_line_offsets);
238
+
239
+ classes.push({
240
+ name: key.name,
241
+ line,
242
+ column,
243
+ offset,
244
+ length: key.name.length,
245
+ sourceOffset,
246
+ });
247
+ }
248
+ }
249
+ }
250
+ break;
251
+ }
252
+
253
+ case 'ArrayExpression': {
254
+ // Dynamic: class={['foo', { bar: true }]}
255
+ for (const el of node.elements) {
256
+ if (el) {
257
+ classes.push(...extract_classes(el, src_to_gen_map, gen_line_offsets, src_line_offsets));
258
+ }
259
+ }
260
+ break;
261
+ }
262
+
263
+ case 'ConditionalExpression': {
264
+ // Conditional: class={@show ? 'active' : 'inactive'}
265
+ if (node.consequent) {
266
+ classes.push(
267
+ ...extract_classes(node.consequent, src_to_gen_map, gen_line_offsets, src_line_offsets),
268
+ );
269
+ }
270
+ if (node.alternate) {
271
+ classes.push(
272
+ ...extract_classes(node.alternate, src_to_gen_map, gen_line_offsets, src_line_offsets),
273
+ );
274
+ }
275
+ break;
276
+ }
277
+
278
+ case 'LogicalExpression': {
279
+ // Logical: class={[@show && 'active']}
280
+ if (node.operator === '&&' && node.right) {
281
+ classes.push(
282
+ ...extract_classes(node.right, src_to_gen_map, gen_line_offsets, src_line_offsets),
283
+ );
284
+ } else if (node.operator === '||') {
285
+ if (node.left) {
286
+ classes.push(
287
+ ...extract_classes(node.left, src_to_gen_map, gen_line_offsets, src_line_offsets),
288
+ );
289
+ }
290
+ if (node.right) {
291
+ classes.push(
292
+ ...extract_classes(node.right, src_to_gen_map, gen_line_offsets, src_line_offsets),
293
+ );
294
+ }
295
+ }
296
+ break;
297
+ }
298
+ }
299
+
300
+ return classes;
301
+ }
106
302
 
107
303
  /**
108
304
  * Create Volar mappings by walking the transformed AST
@@ -110,7 +306,7 @@ function extractCssSourceRegions(ast, source, source_line_offsets) {
110
306
  * @param {AST.Node} ast_from_source - The original AST from source
111
307
  * @param {string} source - Original source code
112
308
  * @param {string} generated_code - Generated code (returned in output, not used for searching)
113
- * @param {object} esrap_source_map - Esrap source map for accurate position lookup
309
+ * @param {SourceMapMappings} source_map - Esrap source map for accurate position lookup
114
310
  * @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
115
311
  * @param {number[]} line_offsets - Pre-computed line offsets array for generated code
116
312
  * @returns {VolarMappingsResult}
@@ -120,7 +316,7 @@ export function convert_source_map_to_mappings(
120
316
  ast_from_source,
121
317
  source,
122
318
  generated_code,
123
- esrap_source_map,
319
+ source_map,
124
320
  post_processing_changes,
125
321
  line_offsets,
126
322
  ) {
@@ -128,7 +324,8 @@ export function convert_source_map_to_mappings(
128
324
  const mappings = [];
129
325
  let isImportDeclarationPresent = false;
130
326
 
131
- const source_line_offsets = build_line_offsets(source);
327
+ const src_line_offsets = build_line_offsets(source);
328
+ const gen_line_offsets = build_line_offsets(generated_code);
132
329
 
133
330
  /**
134
331
  * Convert generated line/column to byte offset using pre-computed line_offsets
@@ -141,25 +338,24 @@ export function convert_source_map_to_mappings(
141
338
  return line_offsets[line - 1] + column;
142
339
  };
143
340
 
144
- const adjusted_source_map = build_source_to_generated_map(
145
- esrap_source_map,
341
+ const [src_to_gen_map] = build_src_to_gen_map(
342
+ source_map,
146
343
  post_processing_changes,
147
344
  line_offsets,
345
+ generated_code,
148
346
  );
149
347
 
150
- // Collect text tokens from AST nodes
151
- // All tokens must have source/generated text and loc property for accurate positioning
152
- /**
153
- * @type {Array<{
154
- * source: string | null | undefined,
155
- * generated: string,
156
- * is_full_import_statement?: boolean,
157
- * loc: AST.SourceLocation,
158
- * end_loc?: AST.SourceLocation,
159
- * metadata?: PluginActionOverrides
160
- * }>}
161
- */
348
+ /** @type {Token[]} */
162
349
  const tokens = [];
350
+ /** @type {CssSourceRegion[]} */
351
+ const css_regions = [];
352
+ /** @type {CssElementInfo} */
353
+ const css_element_info = new Map();
354
+
355
+ visit_source_ast(ast_from_source, src_line_offsets, {
356
+ regions: css_regions,
357
+ css_element_info,
358
+ });
163
359
 
164
360
  // We have to visit everything in generated order to maintain correct indices
165
361
 
@@ -310,11 +506,65 @@ export function convert_source_map_to_mappings(
310
506
  visit(node.value);
311
507
  }
312
508
  } else {
313
- if (node.name) {
314
- visit(node.name);
315
- }
316
- if (node.value) {
317
- visit(node.value);
509
+ const attr =
510
+ node.name.name === 'class' && node.value?.type === 'JSXExpressionContainer'
511
+ ? node.value.expression
512
+ : node.value;
513
+
514
+ const css = attr
515
+ ? css_element_info.get(`${attr.loc?.start.line}:${attr.loc?.start.column}`)
516
+ : null;
517
+
518
+ if (attr && css) {
519
+ // Extract class names from the attribute value
520
+ const classes = extract_classes(
521
+ attr,
522
+ src_to_gen_map,
523
+ gen_line_offsets,
524
+ src_line_offsets,
525
+ );
526
+
527
+ // For each class name, look up CSS location and create token
528
+ for (const { name, line, column, offset, sourceOffset, length } of classes) {
529
+ const cssLocation = css.scopedClasses.get(name);
530
+
531
+ if (!cssLocation) {
532
+ continue;
533
+ }
534
+
535
+ mappings.push({
536
+ sourceOffsets: [sourceOffset],
537
+ generatedOffsets: [offset],
538
+ lengths: [length],
539
+ data: {
540
+ ...mapping_data,
541
+ customData: {
542
+ generatedLengths: [length],
543
+ hover: {
544
+ contents:
545
+ '```css\n.' +
546
+ name +
547
+ '\n```\n\nCSS class selector.\n\nUse **Cmd+Click** (macOS) or **Ctrl+Click** (Windows/Linux) to navigate to its definition.',
548
+ },
549
+ definition: {
550
+ description: `CSS class selector for '.${name}'`,
551
+ location: {
552
+ embeddedId: get_style_region_id(css.hash),
553
+ start: cssLocation.start,
554
+ end: cssLocation.end,
555
+ },
556
+ },
557
+ },
558
+ },
559
+ });
560
+ }
561
+ } else {
562
+ if (node.name) {
563
+ visit(node.name);
564
+ }
565
+ if (node.value) {
566
+ visit(node.value);
567
+ }
318
568
  }
319
569
  }
320
570
  return;
@@ -1377,7 +1627,7 @@ export function convert_source_map_to_mappings(
1377
1627
  const source_start = loc_to_offset(
1378
1628
  token.loc.start.line,
1379
1629
  token.loc.start.column,
1380
- source_line_offsets,
1630
+ src_line_offsets,
1381
1631
  );
1382
1632
 
1383
1633
  let source_length = source_text.length;
@@ -1389,15 +1639,15 @@ export function convert_source_map_to_mappings(
1389
1639
 
1390
1640
  if (token.is_full_import_statement) {
1391
1641
  const end_loc = /** @type {AST.SourceLocation} */ (token.end_loc).end;
1392
- const source_end = loc_to_offset(end_loc.line, end_loc.column, source_line_offsets);
1642
+ const source_end = loc_to_offset(end_loc.line, end_loc.column, src_line_offsets);
1393
1643
 
1394
1644
  // Look up where import keyword and source literal map to in generated code
1395
1645
  const gen_start_pos = get_generated_position(
1396
1646
  token.loc.start.line,
1397
1647
  token.loc.start.column,
1398
- adjusted_source_map,
1648
+ src_to_gen_map,
1399
1649
  );
1400
- const gen_end_pos = get_generated_position(end_loc.line, end_loc.column, adjusted_source_map);
1650
+ const gen_end_pos = get_generated_position(end_loc.line, end_loc.column, src_to_gen_map);
1401
1651
 
1402
1652
  gen_start = gen_loc_to_offset(gen_start_pos.line, gen_start_pos.column);
1403
1653
  const gen_end = gen_loc_to_offset(gen_end_pos.line, gen_end_pos.column);
@@ -1418,7 +1668,7 @@ export function convert_source_map_to_mappings(
1418
1668
  const gen_line_col = get_generated_position(
1419
1669
  token.loc.start.line,
1420
1670
  token.loc.start.column,
1421
- adjusted_source_map,
1671
+ src_to_gen_map,
1422
1672
  );
1423
1673
  gen_start = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
1424
1674
 
@@ -1479,10 +1729,11 @@ export function convert_source_map_to_mappings(
1479
1729
  });
1480
1730
  }
1481
1731
 
1482
- /** @type {{ cssMappings: CodeMapping[], cssSources: string[] }} */
1483
- const cssResult = { cssMappings: [], cssSources: [] };
1484
- for (const region of extractCssSourceRegions(ast_from_source, source, source_line_offsets)) {
1485
- cssResult.cssMappings.push({
1732
+ /** @type {CodeMapping[]} */
1733
+ const cssMappings = [];
1734
+ for (let i = 0; i < css_regions.length; i++) {
1735
+ const region = css_regions[i];
1736
+ cssMappings.push({
1486
1737
  sourceOffsets: [region.start],
1487
1738
  generatedOffsets: [0],
1488
1739
  lengths: [region.content.length],
@@ -1490,16 +1741,16 @@ export function convert_source_map_to_mappings(
1490
1741
  ...mapping_data,
1491
1742
  customData: {
1492
1743
  generatedLengths: [region.content.length],
1744
+ embeddedId: region.id,
1745
+ content: region.content,
1493
1746
  },
1494
1747
  },
1495
1748
  });
1496
-
1497
- cssResult.cssSources.push(region.content);
1498
1749
  }
1499
1750
 
1500
1751
  return {
1501
1752
  code: generated_code,
1502
1753
  mappings,
1503
- ...cssResult,
1754
+ cssMappings,
1504
1755
  };
1505
1756
  }
@@ -1,20 +1,22 @@
1
- /** @import * as AST from 'estree' */
2
- /** @import { Visitors } from 'zimmerframe' */
1
+ /**
2
+ @import * as AST from 'estree';
3
+ @import { Visitors } from '#compiler';
4
+ */
3
5
 
4
6
  /**
5
- * @typedef {{
6
- * code: MagicString;
7
- * hash: string;
8
- * minify: boolean;
9
- * selector: string;
10
- * keyframes: Record<string, {
11
- * indexes: number[];
12
- * local: boolean | undefined;
13
- * }>;
14
- * specificity: {
15
- * bumped: boolean
16
- * }
17
- * }} State
7
+ @typedef {{
8
+ code: MagicString;
9
+ hash: string;
10
+ minify: boolean;
11
+ selector: string;
12
+ keyframes: Record<string, {
13
+ indexes: number[];
14
+ local: boolean | undefined;
15
+ }>;
16
+ specificity: {
17
+ bumped: boolean
18
+ }
19
+ }} State
18
20
  */
19
21
 
20
22
  import MagicString from 'magic-string';