ripple 0.2.185 → 0.2.186

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.185",
6
+ "version": "0.2.186",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -86,6 +86,6 @@
86
86
  "vscode-languageserver-types": "^3.17.5"
87
87
  },
88
88
  "peerDependencies": {
89
- "ripple": "0.2.185"
89
+ "ripple": "0.2.186"
90
90
  }
91
91
  }
@@ -24,6 +24,12 @@ export interface CompileResult {
24
24
  css: string;
25
25
  }
26
26
 
27
+ export interface DefinitionLocation {
28
+ embeddedId: string; // e.g., 'style_0', 'style_1'
29
+ start: number; // start offset
30
+ end: number; // end offset
31
+ }
32
+
27
33
  export interface PluginActionOverrides {
28
34
  /** Whether to enable word document highlighting for this mapping */
29
35
  wordHighlight?: {
@@ -40,13 +46,17 @@ export interface PluginActionOverrides {
40
46
  /** Custom definition info for this mapping, false to disable */
41
47
  definition?:
42
48
  | {
43
- description: string;
49
+ description?: string; // just for reference
50
+ // Generic location for embedded content (CSS, etc.)
51
+ location?: DefinitionLocation;
44
52
  }
45
53
  | false;
46
54
  }
47
55
 
48
56
  export interface CustomMappingData extends PluginActionOverrides {
49
57
  generatedLengths: number[];
58
+ embeddedId?: string; // e.g. css regions: 'style_0', 'style_1', etc.
59
+ content?: string; // (e.g., css code)
50
60
  }
51
61
 
52
62
  export interface MappingData extends VolarCodeInformation {
@@ -61,7 +71,6 @@ export interface VolarMappingsResult {
61
71
  code: string;
62
72
  mappings: CodeMapping[];
63
73
  cssMappings: CodeMapping[];
64
- cssSources: string[];
65
74
  }
66
75
 
67
76
  /**
@@ -1,13 +1,10 @@
1
- /** @import * as AST from 'estree' */
2
- /** @import * as ESTreeJSX from 'estree-jsx' */
3
- /** @import { Parse } from '#parser' */
4
-
5
1
  /**
6
- * @import {
7
- * RipplePluginConfig
8
- * } from '#compiler' */
9
-
10
- /** @import { ParseOptions } from 'ripple/compiler' */
2
+ @import * as AST from 'estree'
3
+ @import * as ESTreeJSX from 'estree-jsx'
4
+ @import { Parse } from '#parser'
5
+ @import { RipplePluginConfig } from '#compiler';
6
+ @import { ParseOptions } from 'ripple/compiler'
7
+ */
11
8
 
12
9
  import * as acorn from 'acorn';
13
10
  import { tsPlugin } from '@sveltejs/acorn-typescript';
@@ -1741,6 +1738,7 @@ function RipplePlugin(config) {
1741
1738
  throw new Error('Components can only have one style tag');
1742
1739
  }
1743
1740
  component.css = parsed_css;
1741
+ /** @type {AST.Element} */ (element).metadata.styleScopeHash = parsed_css.hash;
1744
1742
  }
1745
1743
 
1746
1744
  const newLines = content.match(regex_newline_characters)?.length;
@@ -2167,6 +2165,7 @@ function RipplePlugin(config) {
2167
2165
  * @param {string} source
2168
2166
  * @param {AST.CommentWithLocation[]} comments
2169
2167
  * @param {number} [index=0] - Starting index
2168
+ * @returns {{onComment: Parse.Options['onComment'], add_comments: (ast: AST.Node) => void}}
2170
2169
  */
2171
2170
  function get_comment_handlers(source, comments, index = 0) {
2172
2171
  /**
@@ -2230,11 +2229,7 @@ function get_comment_handlers(source, comments, index = 0) {
2230
2229
  context,
2231
2230
  }));
2232
2231
 
2233
- /**
2234
- * @param {AST.Node} ast
2235
- */
2236
2232
  walk(ast, null, {
2237
- /** @param {AST.Node} node */
2238
2233
  _(node, { next, path }) {
2239
2234
  const metadata = node?.metadata;
2240
2235
 
@@ -41,7 +41,7 @@ function is_global(relative_selector) {
41
41
  export function analyze_css(css) {
42
42
  walk(
43
43
  css,
44
- { rule: /** @type {AST.CSS.Rule | null} */ (null) },
44
+ /** @type {{ rule: AST.CSS.Rule | null }} */ ({ rule: null }),
45
45
  {
46
46
  Rule(node, context) {
47
47
  node.metadata.parent_rule = context.state.rule;
@@ -90,7 +90,7 @@ export function analyze_css(css) {
90
90
  // Set the rule metadata before analyzing children
91
91
  node.metadata.rule = context.state.rule;
92
92
 
93
- context.next(); // analyse relevant selectors first
93
+ context.next(); // analyze relevant selectors first
94
94
 
95
95
  {
96
96
  const global = node.children.find(is_global);
@@ -6,7 +6,6 @@
6
6
  AnalysisContext,
7
7
  ScopeInterface,
8
8
  Visitors,
9
- Visitor,
10
9
  } from '#compiler';
11
10
  */
12
11
  /** @import * as AST from 'estree' */
@@ -5,13 +5,17 @@
5
5
  import { walk } from 'zimmerframe';
6
6
  import { is_element_dom_element } from '../../utils.js';
7
7
 
8
- const seen = new Set();
9
8
  const regex_backslash_and_following_character = /\\(.)/g;
10
9
  /** @type {Direction} */
11
10
  const FORWARD = 0;
12
11
  /** @type {Direction} */
13
12
  const BACKWARD = 1;
14
13
 
14
+ // this will be set for every prune_css call
15
+ // since the code is synchronous, this is safe
16
+ /** @type {string} */
17
+ let css_hash;
18
+
15
19
  // CSS selector constants
16
20
  /**
17
21
  * @param {number} start
@@ -196,6 +200,47 @@ function apply_selector(relative_selectors, rule, element, direction) {
196
200
  if (matched) {
197
201
  if (!is_outer_global(relative_selector)) {
198
202
  relative_selector.metadata.scoped = true;
203
+
204
+ // Store scoped class information on element for language server features
205
+ if (!relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) {
206
+ // Extract class selectors from the relative selector
207
+ for (const selector of relative_selector.selectors) {
208
+ if (selector.type === 'ClassSelector') {
209
+ const name = selector.name.replace(regex_backslash_and_following_character, '$1');
210
+
211
+ if (!element.metadata.css) {
212
+ element.metadata.css = {
213
+ scopedClasses: new Map(),
214
+ topScopedClasses: new Map(),
215
+ hash: css_hash,
216
+ };
217
+ }
218
+
219
+ // Store class name → CSS location in scopedClasses
220
+ if (!element.metadata.css.scopedClasses.has(name)) {
221
+ element.metadata.css.scopedClasses.set(name, {
222
+ start: selector.start,
223
+ end: selector.end,
224
+ selector: selector,
225
+ });
226
+ }
227
+
228
+ // Also store in topScopedClasses if standalone
229
+ // Standalone = only this ClassSelector in the RelativeSelector, no pseudo-classes/elements
230
+ const isStandalone =
231
+ relative_selector.selectors.length === 1 &&
232
+ relative_selector.selectors[0] === selector;
233
+
234
+ if (isStandalone && !element.metadata.css.topScopedClasses.has(name)) {
235
+ element.metadata.css.topScopedClasses.set(name, {
236
+ start: selector.start,
237
+ end: selector.end,
238
+ selector: selector,
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
199
244
  }
200
245
 
201
246
  element.metadata.scoped = true;
@@ -1005,6 +1050,8 @@ function rule_has_animation(rule) {
1005
1050
  * @return {void}
1006
1051
  */
1007
1052
  export function prune_css(css, element) {
1053
+ css_hash = css.hash;
1054
+
1008
1055
  /** @type {Visitors<AST.CSS.Node, null>} */
1009
1056
  const visitors = {
1010
1057
  Rule(node, context) {
@@ -1017,8 +1064,6 @@ export function prune_css(css, element) {
1017
1064
  ComplexSelector(node, context) {
1018
1065
  const selectors = get_relative_selectors(node);
1019
1066
 
1020
- seen.clear();
1021
-
1022
1067
  const rule = /** @type {AST.CSS.Rule} */ (node.metadata.rule);
1023
1068
 
1024
1069
  if (apply_selector(selectors, rule, element, BACKWARD) || rule_has_animation(rule)) {
@@ -1,8 +1,7 @@
1
- /** @import * as AST from 'estree' */
2
- /** @import * as ESTreeJSX from 'estree-jsx' */
3
- /** @import { SourceMapMappings } from '@jridgewell/sourcemap-codec' */
4
- /** @import * as ESRap from 'esrap' */
5
1
  /**
2
+ @import * as AST from 'estree';
3
+ @import * as ESTreeJSX from 'estree-jsx';
4
+ @import { SourceMapMappings } from '@jridgewell/sourcemap-codec';
6
5
  @import {
7
6
  AnalysisResult,
8
7
  TransformClientContext,
@@ -14,8 +13,10 @@
14
13
  } from '#compiler';
15
14
  */
16
15
 
17
- /** @typedef {Map<number, {offset: number, delta: number}>} PostProcessingChanges */
18
- /** @typedef {number[]} LineOffsets */
16
+ /**
17
+ @typedef {Map<number, {offset: number, delta: number}>} PostProcessingChanges;
18
+ @typedef {number[]} LineOffsets;
19
+ */
19
20
 
20
21
  import { walk } from 'zimmerframe';
21
22
  import path from 'node:path';
@@ -2996,7 +2997,7 @@ function create_tsx_with_typescript_support() {
2996
2997
  context.write(node.computed ? ']: ' : ': ');
2997
2998
  context.visit(node.value);
2998
2999
  } else {
2999
- base_tsx.Property?.(node, /** @type {ESRap.} */ (context));
3000
+ base_tsx.Property?.(node, context);
3000
3001
  }
3001
3002
  } else {
3002
3003
  // Use default handler for non-component properties
@@ -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';
@@ -1,9 +1,56 @@
1
1
  import { decode } from '@jridgewell/sourcemap-codec';
2
2
 
3
- /** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
3
+ /**
4
+ @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js';
5
+ @import * as AST from 'estree';
6
+ */
7
+
8
+ /**
9
+ @typedef {{
10
+ line: number,
11
+ column: number,
12
+ end_line: number,
13
+ end_column: number,
14
+ code: string,
15
+ metadata: {
16
+ css?: AST.Element['metadata']['css']
17
+ },
18
+ }} CodePosition
19
+
20
+ @typedef {Map<string, CodePosition[]>} CodeToGeneratedMap
21
+ @typedef {Map<string, {line: number, column: number}[]>} GeneratedToSourceMap
22
+ */
23
+
24
+ /**
25
+ * Convert byte offset to line/column
26
+ * @param {number} offset
27
+ * @param {LineOffsets} line_offsets
28
+ * @returns {{ line: number, column: number }}
29
+ */
30
+ export const offset_to_line_col = (offset, line_offsets) => {
31
+ // Binary search
32
+ let left = 0;
33
+ let right = line_offsets.length - 1;
34
+ let line = 1;
35
+
36
+ while (left <= right) {
37
+ const mid = Math.floor((left + right) / 2);
38
+ if (
39
+ offset >= line_offsets[mid] &&
40
+ (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
41
+ ) {
42
+ line = mid + 1;
43
+ break;
44
+ } else if (offset < line_offsets[mid]) {
45
+ right = mid - 1;
46
+ } else {
47
+ left = mid + 1;
48
+ }
49
+ }
4
50
 
5
- /** @typedef {{line: number, column: number}} GeneratedPosition */
6
- /** @typedef {Map<string, GeneratedPosition[]>} SourceToGeneratedMap */
51
+ const column = offset - line_offsets[line - 1];
52
+ return { line, column };
53
+ };
7
54
 
8
55
  /**
9
56
  * Build a source-to-generated position lookup map from an esrap source map
@@ -11,11 +58,19 @@ import { decode } from '@jridgewell/sourcemap-codec';
11
58
  * @param {object} source_map - The source map object from esrap (v3 format)
12
59
  * @param {PostProcessingChanges} post_processing_changes - Optional post-processing changes to apply
13
60
  * @param {LineOffsets} line_offsets - Pre-computed line offsets array
14
- * @returns {SourceToGeneratedMap} Map from "sourceLine:sourceColumn" to array of generated positions
61
+ * @param {string} generated_code - The final generated code (after post-processing)
62
+ * @returns {[CodeToGeneratedMap, GeneratedToSourceMap]} Tuple of [source-to-generated map, generated-to-source map]
15
63
  */
16
- export function build_source_to_generated_map(source_map, post_processing_changes, line_offsets) {
17
- /** @type {SourceToGeneratedMap} */
64
+ export function build_src_to_gen_map(
65
+ source_map,
66
+ post_processing_changes,
67
+ line_offsets,
68
+ generated_code,
69
+ ) {
70
+ /** @type {CodeToGeneratedMap} */
18
71
  const map = new Map();
72
+ /** @type {GeneratedToSourceMap} */
73
+ const reverse_map = new Map();
19
74
 
20
75
  // Decode the VLQ-encoded mappings string
21
76
  // @ts-ignore
@@ -31,101 +86,120 @@ export function build_source_to_generated_map(source_map, post_processing_change
31
86
  return line_offsets[line - 1] + column;
32
87
  };
33
88
 
34
- /**
35
- * Convert byte offset to line/column
36
- * @param {number} offset
37
- * @returns {{ line: number, column: number }}
38
- */
39
- const offset_to_line_col = (offset) => {
40
- // Binary search
41
- let left = 0;
42
- let right = line_offsets.length - 1;
43
- let line = 1;
44
-
45
- while (left <= right) {
46
- const mid = Math.floor((left + right) / 2);
47
- if (
48
- offset >= line_offsets[mid] &&
49
- (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
50
- ) {
51
- line = mid + 1;
52
- break;
53
- } else if (offset < line_offsets[mid]) {
54
- right = mid - 1;
55
- } else {
56
- left = mid + 1;
57
- }
58
- }
59
-
60
- const column = offset - line_offsets[line - 1];
61
- return { line, column };
62
- };
89
+ // Apply post-processing adjustments to all segments first
90
+ /** @type {Array<Array<{line: number, column: number, sourceLine: number, sourceColumn: number}>>} */
91
+ const adjusted_segments = [];
63
92
 
64
- // decoded is an array of lines, each line is an array of segments
65
- // Each segment is [generatedColumn, sourceIndex, sourceLine, sourceColumn, nameIndex?]
66
93
  for (let generated_line = 0; generated_line < decoded.length; generated_line++) {
67
94
  const line = decoded[generated_line];
95
+ adjusted_segments[generated_line] = [];
68
96
 
69
97
  for (const segment of line) {
70
98
  if (segment.length >= 4) {
71
- let generated_column = segment[0];
72
- // just keeping this unused for context
73
- // const source_index = segment[1]; // which source file (we only have one)
74
- const source_line = /** @type {number} */ (segment[2]);
75
- const source_column = /** @type {number} */ (segment[3]);
76
-
77
- // Apply post-processing adjustments if needed
78
99
  let adjusted_line = generated_line + 1;
79
- let adjusted_column = generated_column;
100
+ let adjusted_column = segment[0];
80
101
 
81
102
  if (post_processing_changes) {
82
103
  const line_change = post_processing_changes.get(adjusted_line);
83
104
 
84
105
  if (line_change) {
85
- // Check if this position is affected by the change
86
106
  const pos_offset = line_col_to_byte_offset(adjusted_line, adjusted_column);
87
107
 
88
108
  if (pos_offset >= line_change.offset) {
89
- // Position is on or after the change - apply delta
90
109
  const adjusted_offset = pos_offset + line_change.delta;
91
- const adjusted_pos = offset_to_line_col(adjusted_offset);
110
+ const adjusted_pos = offset_to_line_col(adjusted_offset, line_offsets);
92
111
  adjusted_line = adjusted_pos.line;
93
112
  adjusted_column = adjusted_pos.column;
94
113
  }
95
114
  }
96
115
  }
97
116
 
98
- // Create key from source position (1-indexed line, 0-indexed column)
99
- const key = `${source_line + 1}:${source_column}`;
117
+ adjusted_segments[generated_line].push({
118
+ line: adjusted_line,
119
+ column: adjusted_column,
120
+ sourceLine: /** @type {number} */ (segment[2]),
121
+ sourceColumn: /** @type {number} */ (segment[3]),
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ // Now build the map using adjusted positions
128
+ for (let line_idx = 0; line_idx < adjusted_segments.length; line_idx++) {
129
+ const line_segments = adjusted_segments[line_idx];
130
+
131
+ for (let seg_idx = 0; seg_idx < line_segments.length; seg_idx++) {
132
+ const segment = line_segments[seg_idx];
133
+ const line = segment.line;
134
+ const column = segment.column;
135
+
136
+ // Determine end position using next segment
137
+ let end_line = line;
138
+ let end_column = column;
139
+
140
+ // Look for next segment to determine end position
141
+ if (seg_idx + 1 < line_segments.length) {
142
+ // Next segment on same line
143
+ const next_segment = line_segments[seg_idx + 1];
144
+ end_line = next_segment.line;
145
+ end_column = next_segment.column;
146
+ } else if (
147
+ line_idx + 1 < adjusted_segments.length &&
148
+ adjusted_segments[line_idx + 1].length > 0
149
+ ) {
150
+ // Look at first segment of next line
151
+ const next_segment = adjusted_segments[line_idx + 1][0];
152
+ end_line = next_segment.line;
153
+ end_column = next_segment.column;
154
+ }
155
+
156
+ // Extract code snippet
157
+ const start_offset = line_col_to_byte_offset(line, column);
158
+ const end_offset = line_col_to_byte_offset(end_line, end_column);
159
+ const code_snippet = generated_code.slice(start_offset, end_offset);
100
160
 
101
- // Store adjusted generated position
102
- const gen_pos = { line: adjusted_line, column: adjusted_column };
161
+ // Create key from source position (1-indexed line, 0-indexed column)
162
+ segment.sourceLine += 1;
163
+ const key = `${segment.sourceLine}:${segment.sourceColumn}`;
103
164
 
104
- if (!map.has(key)) {
105
- map.set(key, []);
106
- }
107
- /** @type {GeneratedPosition[]} */ (map.get(key)).push(gen_pos);
165
+ // Store adjusted generated position with code snippet
166
+ const gen_pos = { line, column, end_line, end_column, code: code_snippet, metadata: {} };
167
+
168
+ if (!map.has(key)) {
169
+ map.set(key, []);
170
+ }
171
+ /** @type {CodePosition[]} */ (map.get(key)).push(gen_pos);
172
+
173
+ // Store reverse mapping (generated to source)
174
+ const gen_key = `${gen_pos.line}:${gen_pos.column}`;
175
+
176
+ if (!reverse_map.has(gen_key)) {
177
+ reverse_map.set(gen_key, []);
108
178
  }
179
+ reverse_map.get(gen_key)?.push({
180
+ line: segment.sourceLine,
181
+ column: segment.sourceColumn,
182
+ });
109
183
  }
110
184
  }
111
185
 
112
- return map;
186
+ return [map, reverse_map];
113
187
  }
114
188
 
115
189
  /**
116
190
  * Look up generated position for a given source position
117
- * @param {number} source_line - 1-based line number in source
118
- * @param {number} source_column - 0-based column number in source
119
- * @param {SourceToGeneratedMap} source_to_gen_map - Lookup map
120
- * @returns {{line: number, column: number}} Generated position
191
+ * @param {number} src_line - 1-based line number in source
192
+ * @param {number} src_column - 0-based column number in source
193
+ * @param {CodeToGeneratedMap} src_to_gen_map - Lookup map
194
+ * @returns {CodePosition} Generated position
121
195
  */
122
- export function get_generated_position(source_line, source_column, source_to_gen_map) {
123
- const key = `${source_line}:${source_column}`;
124
- const positions = source_to_gen_map.get(key);
196
+ export function get_generated_position(src_line, src_column, src_to_gen_map) {
197
+ const key = `${src_line}:${src_column}`;
198
+ const positions = src_to_gen_map.get(key);
125
199
 
126
200
  if (!positions || positions.length === 0) {
127
201
  // No mapping found in source map - this shouldn't happen since all tokens should have mappings
128
- throw new Error(`No source map entry for position "${source_line}:${source_column}"`);
202
+ throw new Error(`No source map entry for position "${src_line}:${src_column}"`);
129
203
  }
130
204
 
131
205
  // If multiple generated positions map to same source, return the first
@@ -226,6 +226,28 @@ declare module 'estree' {
226
226
  loc: SourceLocation;
227
227
  metadata: BaseNodeMetaData & {
228
228
  ts_name?: string;
229
+ // for <style> tag
230
+ styleScopeHash?: string;
231
+ // for elements with scoped style classes
232
+ css?: {
233
+ scopedClasses: Map<
234
+ string,
235
+ {
236
+ start: number;
237
+ end: number;
238
+ selector: CSS.ClassSelector;
239
+ }
240
+ >;
241
+ topScopedClasses: Map<
242
+ string,
243
+ {
244
+ start: number;
245
+ end: number;
246
+ selector: CSS.ClassSelector;
247
+ }
248
+ >;
249
+ hash: string;
250
+ };
229
251
  };
230
252
 
231
253
  // currently only for <style> and <script> tags
@@ -56,6 +56,12 @@ declare module 'zimmerframe' {
56
56
  state: any,
57
57
  visitors: RippleCompiler.Visitors<AST.Node, any>,
58
58
  ): AST.Node;
59
+
60
+ export function walk(
61
+ node: AST.CSS.Node,
62
+ state: any,
63
+ visitors: RippleCompiler.Visitors<(AST.CSS.Node), any>,
64
+ ): AST.CSS.Node;
59
65
  }
60
66
 
61
67
  export namespace Parse {
@@ -1,5 +1,7 @@
1
- /** @import { CommonContext, NameSpace, ScopeInterface } from '#compiler' */
2
- /** @import * as AST from 'estree' */
1
+ /**
2
+ @import * as AST from 'estree';
3
+ @import { CommonContext, NameSpace, ScopeInterface } from '#compiler';
4
+ */
3
5
 
4
6
  import { build_assignment_value, extract_paths } from '../utils/ast.js';
5
7
  import * as b from '../utils/builders.js';