ripple 0.2.173 → 0.2.175

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.173",
6
+ "version": "0.2.175",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -78,9 +78,10 @@
78
78
  "devDependencies": {
79
79
  "@types/estree": "^1.0.8",
80
80
  "@types/node": "^24.3.0",
81
- "typescript": "^5.9.2"
81
+ "typescript": "^5.9.2",
82
+ "@volar/language-core": "~2.4.23"
82
83
  },
83
84
  "peerDependencies": {
84
- "ripple": "0.2.173"
85
+ "ripple": "0.2.175"
85
86
  }
86
87
  }
@@ -25,20 +25,22 @@ export function parse(source) {
25
25
  export function compile(source, filename, options = {}) {
26
26
  const ast = parse_module(source);
27
27
  const analysis = analyze(ast, filename, options);
28
- const result = options.mode === 'server'
29
- ? transform_server(filename, source, analysis)
30
- : transform_client(filename, source, analysis, false);
28
+ const result =
29
+ options.mode === 'server'
30
+ ? transform_server(filename, source, analysis)
31
+ : transform_client(filename, source, analysis, false);
31
32
 
32
33
  return result;
33
34
  }
34
35
 
35
36
  /** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
37
+ /** @import { MappingsResult } from './phases/3-transform/segments.js' */
36
38
 
37
39
  /**
38
40
  * Compile Ripple component to Volar virtual code with TypeScript mappings
39
41
  * @param {string} source
40
42
  * @param {string} filename
41
- * @returns {object} Volar mappings object
43
+ * @returns {MappingsResult} Volar mappings object
42
44
  */
43
45
  export function compile_to_volar_mappings(source, filename) {
44
46
  const ast = parse_module(source);
@@ -51,7 +53,7 @@ export function compile_to_volar_mappings(source, filename) {
51
53
  source,
52
54
  transformed.js.code,
53
55
  transformed.js.map,
54
- /** @type {PostProcessingChanges} */(transformed.js.post_processing_changes),
55
- /** @type {LineOffsets} */(transformed.js.line_offsets)
56
+ /** @type {PostProcessingChanges} */ (transformed.js.post_processing_changes),
57
+ /** @type {LineOffsets} */ (transformed.js.line_offsets),
56
58
  );
57
59
  }
@@ -2527,6 +2527,62 @@ function create_tsx_with_typescript_support() {
2527
2527
  FunctionExpression(node, context) {
2528
2528
  handle_function(node, context);
2529
2529
  },
2530
+ // Custom handler for ImportDeclaration to ensure 'import' keyword has source mapping
2531
+ // This creates a source map entry at the start of the import statement
2532
+ // Esrap's default handler writes 'import' without passing the node, so no source map entry
2533
+ ImportDeclaration(node, context) {
2534
+ // Write 'import' keyword with node location for source mapping
2535
+ context.write('import', node);
2536
+ context.write(' ');
2537
+
2538
+ // Write specifiers - handle default, namespace, and named imports
2539
+ if (node.specifiers && node.specifiers.length > 0) {
2540
+ let default_specifier = null;
2541
+ let namespace_specifier = null;
2542
+ const named_specifiers = [];
2543
+
2544
+ for (const spec of node.specifiers) {
2545
+ if (spec.type === 'ImportDefaultSpecifier') {
2546
+ default_specifier = spec;
2547
+ } else if (spec.type === 'ImportNamespaceSpecifier') {
2548
+ namespace_specifier = spec;
2549
+ } else if (spec.type === 'ImportSpecifier') {
2550
+ named_specifiers.push(spec);
2551
+ }
2552
+ }
2553
+
2554
+ // Write default import
2555
+ if (default_specifier) {
2556
+ context.visit(default_specifier);
2557
+ if (namespace_specifier || named_specifiers.length > 0) {
2558
+ context.write(', ');
2559
+ }
2560
+ }
2561
+
2562
+ // Write namespace import
2563
+ if (namespace_specifier) {
2564
+ context.visit(namespace_specifier);
2565
+ if (named_specifiers.length > 0) {
2566
+ context.write(', ');
2567
+ }
2568
+ }
2569
+
2570
+ // Write named imports
2571
+ if (named_specifiers.length > 0) {
2572
+ context.write('{ ');
2573
+ for (let i = 0; i < named_specifiers.length; i++) {
2574
+ if (i > 0) context.write(', ');
2575
+ context.visit(named_specifiers[i]);
2576
+ }
2577
+ context.write(' }');
2578
+ }
2579
+
2580
+ context.write(' from ');
2581
+ }
2582
+
2583
+ // Write source
2584
+ context.visit(node.source);
2585
+ },
2530
2586
  // Custom handler for TSParenthesizedType: (Type)
2531
2587
  TSParenthesizedType(node, context) {
2532
2588
  context.write('(');
@@ -1,19 +1,28 @@
1
+ /**
2
+ * @typedef {Object} CustomMappingData
3
+ * @property {number[]} generatedLengths
4
+ */
5
+
6
+ /**
7
+ * @typedef {import('estree').Position} Position
8
+ * @typedef {{start: Position, end: Position}} Location
9
+ * @typedef {import('@volar/language-core').CodeMapping} VolarCodeMapping
10
+ * @typedef {VolarCodeMapping['data'] & {customData: CustomMappingData}} MappingData
11
+ * @typedef {VolarCodeMapping & {data: MappingData}} CodeMapping
12
+ * @typedef {{code: string, mappings: CodeMapping[]}} MappingsResult
13
+ */
14
+
1
15
  import { walk } from 'zimmerframe';
2
16
  import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
3
17
 
18
+ /** @type {VolarCodeMapping['data']} */
4
19
  export const mapping_data = {
5
20
  verification: true,
6
21
  completion: true,
7
22
  semantic: true,
8
23
  navigation: true,
9
- rename: true,
10
- codeActions: false, // set to false to disable auto import when importing yourself
11
- formatting: false, // not doing formatting through Volar, using Prettier.
12
- // these 3 below will be true by default
13
- // leaving for reference
14
- // hover: true,
15
- // definition: true,
16
- // references: true,
24
+ structure: true,
25
+ format: false,
17
26
  };
18
27
 
19
28
  /**
@@ -28,14 +37,25 @@ export const mapping_data = {
28
37
  * @param {object} esrap_source_map - Esrap source map for accurate position lookup
29
38
  * @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
30
39
  * @param {number[]} line_offsets - Pre-computed line offsets array for generated code
31
- * @returns {{ code: string, mappings: Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}> }}
40
+ * @returns {MappingsResult}
32
41
  */
33
- export function convert_source_map_to_mappings(ast, source, generated_code, esrap_source_map, post_processing_changes, line_offsets) {
34
- /** @type {Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}>} */
42
+ export function convert_source_map_to_mappings(
43
+ ast,
44
+ source,
45
+ generated_code,
46
+ esrap_source_map,
47
+ post_processing_changes,
48
+ line_offsets,
49
+ ) {
50
+ /** @type {CodeMapping[]} */
35
51
  const mappings = [];
52
+ let isImportDeclarationPresent = false;
36
53
 
37
- // Build line offset maps for source and generated code
38
- // This allows us to convert line/column positions to byte offsets
54
+ /**
55
+ * Converts line/column positions to byte offsets
56
+ * @param {string} text
57
+ * @returns {number[]}
58
+ */
39
59
  const build_line_offsets = (text) => {
40
60
  const offsets = [0]; // Line 1 starts at offset 0
41
61
  for (let i = 0; i < text.length; i++) {
@@ -52,10 +72,14 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
52
72
  * @param {number} line
53
73
  * @param {number} column
54
74
  * @param {number[]} line_offsets
55
- * @returns {number | null}
75
+ * @returns {number}
56
76
  */
57
77
  const loc_to_offset = (line, column, line_offsets) => {
58
- if (line < 1 || line > line_offsets.length) return null;
78
+ if (line < 1 || line > line_offsets.length) {
79
+ throw new Error(
80
+ `Location line or line offsets length is out of bounds, line: ${line}, line offsets length: ${line_offsets.length}`,
81
+ );
82
+ }
59
83
  return line_offsets[line - 1] + column;
60
84
  };
61
85
 
@@ -73,17 +97,20 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
73
97
  const adjusted_source_map = build_source_to_generated_map(
74
98
  esrap_source_map,
75
99
  post_processing_changes,
76
- line_offsets
100
+ line_offsets,
77
101
  );
78
102
 
79
103
  // Collect text tokens from AST nodes
80
104
  // All tokens must have source/generated text and loc property for accurate positioning
81
- /** @type {Array<{
82
- source: string,
83
- generated: string,
84
- is_import_statement?: boolean,
85
- loc: {start: {line: number, column: number}, end: {line: number, column: number}}
86
- }>} */
105
+ /**
106
+ * @type {Array<{
107
+ * source: string,
108
+ * generated: string,
109
+ * is_full_import_statement?: boolean,
110
+ * loc: Location,
111
+ * end_loc?: Location,
112
+ * }>}
113
+ */
87
114
  const tokens = [];
88
115
 
89
116
  // We have to visit everything in generated order to maintain correct indices
@@ -96,11 +123,19 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
96
123
  if (node.name && node.loc) {
97
124
  // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
98
125
  if (node.metadata?.tracked_shorthand) {
99
- tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name, loc: node.loc });
126
+ tokens.push({
127
+ source: node.metadata.tracked_shorthand,
128
+ generated: node.name,
129
+ loc: node.loc,
130
+ });
100
131
  } else if (node.metadata?.is_capitalized) {
101
132
  // This identifier was capitalized during transformation
102
133
  // Map the original lowercase name to the capitalized generated name
103
- tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
134
+ tokens.push({
135
+ source: node.metadata.original_name,
136
+ generated: node.name,
137
+ loc: node.loc,
138
+ });
104
139
  } else {
105
140
  // No transformation - source and generated names are the same
106
141
  tokens.push({ source: node.name, generated: node.name, loc: node.loc });
@@ -111,7 +146,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
111
146
  // JSXIdentifiers can also be capitalized (for dynamic components)
112
147
  if (node.loc && node.name) {
113
148
  if (node.metadata?.is_capitalized) {
114
- tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
149
+ tokens.push({
150
+ source: node.metadata.original_name,
151
+ generated: node.name,
152
+ loc: node.loc,
153
+ });
115
154
  } else {
116
155
  tokens.push({ source: node.name, generated: node.name, loc: node.loc });
117
156
  }
@@ -123,14 +162,17 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
123
162
  }
124
163
  return; // Leaf node, don't traverse further
125
164
  } else if (node.type === 'ImportDeclaration') {
165
+ isImportDeclarationPresent = true;
166
+
126
167
  // Add import declaration as a special token for full-statement mapping
127
168
  // TypeScript reports unused imports with diagnostics covering the entire statement
128
- if (node.loc) {
169
+ if (node.loc && node.source?.loc) {
129
170
  tokens.push({
130
171
  source: '',
131
172
  generated: '',
132
173
  loc: node.loc,
133
- is_import_statement: true
174
+ is_full_import_statement: true,
175
+ end_loc: node.source.loc,
134
176
  });
135
177
  }
136
178
 
@@ -152,7 +194,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
152
194
  visit(node.local);
153
195
  }
154
196
  return;
155
- } else if (node.type === 'ImportDefaultSpecifier' || node.type === 'ImportNamespaceSpecifier') {
197
+ } else if (
198
+ node.type === 'ImportDefaultSpecifier' ||
199
+ node.type === 'ImportNamespaceSpecifier'
200
+ ) {
156
201
  // Just visit local
157
202
  if (node.local) {
158
203
  visit(node.local);
@@ -246,17 +291,32 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
246
291
  }
247
292
 
248
293
  // 3. Push closing tag name (not visited by AST walker)
249
- if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
294
+ if (
295
+ !node.openingElement?.selfClosing &&
296
+ node.closingElement?.name?.type === 'JSXIdentifier'
297
+ ) {
250
298
  const closingNameNode = node.closingElement.name;
251
299
  if (closingNameNode.metadata?.is_capitalized) {
252
- tokens.push({ source: closingNameNode.metadata.original_name, generated: closingNameNode.name, loc: closingNameNode.loc });
300
+ tokens.push({
301
+ source: closingNameNode.metadata.original_name,
302
+ generated: closingNameNode.name,
303
+ loc: closingNameNode.loc,
304
+ });
253
305
  } else {
254
- tokens.push({ source: closingNameNode.name, generated: closingNameNode.name, loc: closingNameNode.loc });
306
+ tokens.push({
307
+ source: closingNameNode.name,
308
+ generated: closingNameNode.name,
309
+ loc: closingNameNode.loc,
310
+ });
255
311
  }
256
312
  }
257
313
 
258
314
  return;
259
- } else if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
315
+ } else if (
316
+ node.type === 'FunctionDeclaration' ||
317
+ node.type === 'FunctionExpression' ||
318
+ node.type === 'ArrowFunctionExpression'
319
+ ) {
260
320
  // Add function/component keyword token
261
321
  if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
262
322
  const source_keyword = node.metadata?.was_component ? 'component' : 'function';
@@ -266,8 +326,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
266
326
  generated: 'function',
267
327
  loc: {
268
328
  start: { line: node.loc.start.line, column: node.loc.start.column },
269
- end: { line: node.loc.start.line, column: node.loc.start.column + source_keyword.length }
270
- }
329
+ end: {
330
+ line: node.loc.start.line,
331
+ column: node.loc.start.column + source_keyword.length,
332
+ },
333
+ },
271
334
  });
272
335
  }
273
336
 
@@ -654,7 +717,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
654
717
  }
655
718
  }
656
719
  return;
657
- } else if (node.type === 'JSXClosingElement' || node.type === 'JSXClosingFragment' || node.type === 'JSXOpeningFragment') {
720
+ } else if (
721
+ node.type === 'JSXClosingElement' ||
722
+ node.type === 'JSXClosingFragment' ||
723
+ node.type === 'JSXOpeningFragment'
724
+ ) {
658
725
  // These are handled by their parent nodes
659
726
  return;
660
727
  } else if (node.type === 'JSXMemberExpression') {
@@ -736,7 +803,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
736
803
  }
737
804
  // Skip typeAnnotation
738
805
  return;
739
- } else if (node.type === 'TSTypeParameterInstantiation' || node.type === 'TSTypeParameterDeclaration') {
806
+ } else if (
807
+ node.type === 'TSTypeParameterInstantiation' ||
808
+ node.type === 'TSTypeParameterDeclaration'
809
+ ) {
740
810
  // Generic type parameters - visit to collect type variable names
741
811
  if (node.params) {
742
812
  for (const param of node.params) {
@@ -881,7 +951,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
881
951
  visit(node.typeAnnotation);
882
952
  }
883
953
  return;
884
- } else if (node.type === 'TSCallSignatureDeclaration' || node.type === 'TSConstructSignatureDeclaration') {
954
+ } else if (
955
+ node.type === 'TSCallSignatureDeclaration' ||
956
+ node.type === 'TSConstructSignatureDeclaration'
957
+ ) {
885
958
  // Call or construct signature
886
959
  if (node.typeParameters) {
887
960
  visit(node.typeParameters);
@@ -1078,7 +1151,22 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1078
1151
  visit(node.typeAnnotation);
1079
1152
  }
1080
1153
  return;
1081
- } else if (node.type === 'TSAnyKeyword' || node.type === 'TSUnknownKeyword' || node.type === 'TSNumberKeyword' || node.type === 'TSObjectKeyword' || node.type === 'TSBooleanKeyword' || node.type === 'TSBigIntKeyword' || node.type === 'TSStringKeyword' || node.type === 'TSSymbolKeyword' || node.type === 'TSVoidKeyword' || node.type === 'TSUndefinedKeyword' || node.type === 'TSNullKeyword' || node.type === 'TSNeverKeyword' || node.type === 'TSThisType' || node.type === 'TSIntrinsicKeyword') {
1154
+ } else if (
1155
+ node.type === 'TSAnyKeyword' ||
1156
+ node.type === 'TSUnknownKeyword' ||
1157
+ node.type === 'TSNumberKeyword' ||
1158
+ node.type === 'TSObjectKeyword' ||
1159
+ node.type === 'TSBooleanKeyword' ||
1160
+ node.type === 'TSBigIntKeyword' ||
1161
+ node.type === 'TSStringKeyword' ||
1162
+ node.type === 'TSSymbolKeyword' ||
1163
+ node.type === 'TSVoidKeyword' ||
1164
+ node.type === 'TSUndefinedKeyword' ||
1165
+ node.type === 'TSNullKeyword' ||
1166
+ node.type === 'TSNeverKeyword' ||
1167
+ node.type === 'TSThisType' ||
1168
+ node.type === 'TSIntrinsicKeyword'
1169
+ ) {
1082
1170
  // Primitive type keywords - leaf nodes, no children
1083
1171
  return;
1084
1172
  } else if (node.type === 'TSDeclareFunction') {
@@ -1120,67 +1208,77 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1120
1208
  }
1121
1209
 
1122
1210
  throw new Error(`Unhandled AST node type in mapping walker: ${node.type}`);
1123
- }
1211
+ },
1124
1212
  });
1125
1213
 
1126
- // Process each token in order
1127
- // All tokens now have .loc property - no need for fallback logic
1128
1214
  for (const token of tokens) {
1129
1215
  const source_text = token.source;
1216
+ const gen_text = token.generated;
1130
1217
 
1131
- // Handle import statement full-statement mapping
1132
- if (token.is_import_statement) {
1133
- // Get source position from start
1134
- const source_start = loc_to_offset(token.loc.start.line, token.loc.start.column, source_line_offsets);
1135
- const source_end = loc_to_offset(token.loc.end.line, token.loc.end.column, source_line_offsets);
1218
+ const source_start = loc_to_offset(
1219
+ token.loc.start.line,
1220
+ token.loc.start.column,
1221
+ source_line_offsets,
1222
+ );
1136
1223
 
1137
- // Get generated positions using source map
1138
- const gen_start_pos = get_generated_position(token.loc.start.line, token.loc.start.column, adjusted_source_map);
1139
- const gen_end_pos = get_generated_position(token.loc.end.line, token.loc.end.column, adjusted_source_map);
1224
+ let source_length = source_text.length;
1225
+ let gen_length = gen_text.length;
1226
+ /** @type {MappingData} */
1227
+ let data;
1228
+ /** @type {number} */
1229
+ let gen_start;
1140
1230
 
1141
- if (source_start !== null && source_end !== null && gen_start_pos && gen_end_pos) {
1142
- // Convert generated line:col to byte offsets
1143
- const gen_start = gen_loc_to_offset(gen_start_pos.line, gen_start_pos.column);
1144
- const gen_end = gen_loc_to_offset(gen_end_pos.line, gen_end_pos.column);
1231
+ if (token.is_full_import_statement) {
1232
+ const end_loc = /** @type {Location} */ (token.end_loc).end;
1233
+ const source_end = loc_to_offset(end_loc.line, end_loc.column, source_line_offsets);
1145
1234
 
1146
- const source_length = source_end - source_start;
1147
- const gen_length = gen_end - gen_start;
1235
+ // Look up where import keyword and source literal map to in generated code
1236
+ const gen_start_pos = get_generated_position(
1237
+ token.loc.start.line,
1238
+ token.loc.start.column,
1239
+ adjusted_source_map,
1240
+ );
1241
+ const gen_end_pos = get_generated_position(end_loc.line, end_loc.column, adjusted_source_map);
1148
1242
 
1149
- mappings.push({
1150
- sourceOffsets: [source_start],
1151
- generatedOffsets: [gen_start],
1152
- lengths: [Math.min(source_length, gen_length)],
1153
- data: {
1154
- // only verification (diagnostics) to avoid duplicate hover/completion
1155
- verification: true
1156
- },
1157
- });
1158
- }
1159
- continue;
1160
- }
1243
+ gen_start = gen_loc_to_offset(gen_start_pos.line, gen_start_pos.column);
1244
+ const gen_end = gen_loc_to_offset(gen_end_pos.line, gen_end_pos.column);
1161
1245
 
1162
- // Use .loc to get the exact source position
1163
- const source_pos = loc_to_offset(token.loc.start.line, token.loc.start.column, source_line_offsets);
1246
+ source_length = source_end - source_start;
1247
+ gen_length = gen_end - gen_start;
1164
1248
 
1165
- // Get generated position using source map
1166
- const gen_line_col = get_generated_position(token.loc.start.line, token.loc.start.column, adjusted_source_map);
1167
- let gen_pos = null;
1168
- if (gen_line_col) {
1169
- // Convert generated line:col to byte offset
1170
- gen_pos = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
1249
+ data = {
1250
+ // we only want verification here, like unused imports
1251
+ // since this is synthetic and otherwise we'll get duplicated actions like intellisense
1252
+ // each imported specifier has its own mapping
1253
+ verification: true,
1254
+ customData: {
1255
+ generatedLengths: [gen_length],
1256
+ },
1257
+ };
1171
1258
  } else {
1172
- // No mapping found in source map - this shouldn't happen since all tokens should have mappings
1173
- console.warn(`[segments.js] No source map entry for token "${source_text}" at ${token.loc.start.line}:${token.loc.start.column}`);
1174
- }
1259
+ const gen_line_col = get_generated_position(
1260
+ token.loc.start.line,
1261
+ token.loc.start.column,
1262
+ adjusted_source_map,
1263
+ );
1264
+ gen_start = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
1175
1265
 
1176
- if (source_pos !== null && gen_pos !== null) {
1177
- mappings.push({
1178
- sourceOffsets: [source_pos],
1179
- generatedOffsets: [gen_pos],
1180
- lengths: [source_text.length],
1181
- data: mapping_data,
1182
- });
1266
+ data = {
1267
+ ...mapping_data,
1268
+ customData: {
1269
+ generatedLengths: [gen_length],
1270
+ },
1271
+ };
1183
1272
  }
1273
+
1274
+ // !IMPORTANT: don't set generatedLengths, otherwise Volar will use that vs our source
1275
+ // We're adding it to our custom metadata instead as we need it for patching positions
1276
+ mappings.push({
1277
+ sourceOffsets: [source_start],
1278
+ generatedOffsets: [gen_start],
1279
+ lengths: [source_length],
1280
+ data,
1281
+ });
1184
1282
  }
1185
1283
 
1186
1284
  // Sort mappings by source offset // Sort mappings by source offset
@@ -1188,16 +1286,17 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1188
1286
 
1189
1287
  // Add a mapping for the very beginning of the file to handle import additions
1190
1288
  // This ensures that code actions adding imports at the top work correctly
1191
- if (mappings.length > 0 && mappings[0].sourceOffsets[0] > 0) {
1289
+ if (!isImportDeclarationPresent && mappings.length > 0 && mappings[0].sourceOffsets[0] > 0) {
1192
1290
  mappings.unshift({
1193
1291
  sourceOffsets: [0],
1194
1292
  generatedOffsets: [0],
1195
1293
  lengths: [1],
1196
1294
  data: {
1197
1295
  ...mapping_data,
1198
- codeActions: true, // auto-import
1199
- rename: false, // avoid rename for a “dummy” mapping
1200
- }
1296
+ customData: {
1297
+ generatedLengths: [1],
1298
+ },
1299
+ },
1201
1300
  });
1202
1301
  }
1203
1302
 
@@ -44,7 +44,10 @@ export function build_source_to_generated_map(source_map, post_processing_change
44
44
 
45
45
  while (left <= right) {
46
46
  const mid = Math.floor((left + right) / 2);
47
- if (offset >= line_offsets[mid] && (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])) {
47
+ if (
48
+ offset >= line_offsets[mid] &&
49
+ (mid === line_offsets.length - 1 || offset < line_offsets[mid + 1])
50
+ ) {
48
51
  line = mid + 1;
49
52
  break;
50
53
  } else if (offset < line_offsets[mid]) {
@@ -114,14 +117,15 @@ export function build_source_to_generated_map(source_map, post_processing_change
114
117
  * @param {number} source_line - 1-based line number in source
115
118
  * @param {number} source_column - 0-based column number in source
116
119
  * @param {SourceToGeneratedMap} source_to_gen_map - Lookup map
117
- * @returns {{line: number, column: number} | null} Generated position or null if not found
120
+ * @returns {{line: number, column: number}} Generated position
118
121
  */
119
122
  export function get_generated_position(source_line, source_column, source_to_gen_map) {
120
123
  const key = `${source_line}:${source_column}`;
121
124
  const positions = source_to_gen_map.get(key);
122
125
 
123
126
  if (!positions || positions.length === 0) {
124
- return null;
127
+ // 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}"`);
125
129
  }
126
130
 
127
131
  // If multiple generated positions map to same source, return the first
@@ -256,10 +256,10 @@ export function bindChecked(maybe_tracked) {
256
256
  throw not_tracked_type_error('bindChecked()');
257
257
  }
258
258
 
259
- const tracked = /** @type {Tracked} */ (maybe_tracked);
259
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
260
260
 
261
261
  return (input) => {
262
- const clear_event = on(input, 'change', () => {
262
+ var clear_event = on(input, 'change', () => {
263
263
  set(tracked, input.checked);
264
264
  });
265
265
 
@@ -281,10 +281,10 @@ export function bindIndeterminate(maybe_tracked) {
281
281
  throw not_tracked_type_error('bindIndeterminate()');
282
282
  }
283
283
 
284
- const tracked = /** @type {Tracked} */ (maybe_tracked);
284
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
285
285
 
286
286
  return (input) => {
287
- const clear_event = on(input, 'change', () => {
287
+ var clear_event = on(input, 'change', () => {
288
288
  set(tracked, input.indeterminate);
289
289
  });
290
290
 
@@ -367,7 +367,7 @@ function bind_element_size(maybe_tracked, type) {
367
367
  );
368
368
 
369
369
  effect(() => {
370
- untrack(() => set(tracked, element[type]));
370
+ set(tracked, element[type]);
371
371
  return unsubscribe;
372
372
  });
373
373
  };
@@ -474,10 +474,10 @@ export function bind_content_editable(maybe_tracked, property) {
474
474
  throw not_tracked_type_error(`bind${property.charAt(0).toUpperCase() + property.slice(1)}()`);
475
475
  }
476
476
 
477
- const tracked = /** @type {Tracked} */ (maybe_tracked);
477
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
478
478
 
479
479
  return (element) => {
480
- const clear_event = on(element, 'input', () => {
480
+ var clear_event = on(element, 'input', () => {
481
481
  set(tracked, element[property]);
482
482
  });
483
483
 
@@ -533,12 +533,22 @@ export function bindFiles(maybe_tracked) {
533
533
  throw not_tracked_type_error('bindFiles()');
534
534
  }
535
535
 
536
- const tracked = /** @type {Tracked} */ (maybe_tracked);
536
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
537
537
 
538
538
  return (input) => {
539
- return on(input, 'change', () => {
539
+ var clear_event = on(input, 'change', () => {
540
540
  set(tracked, input.files);
541
541
  });
542
+
543
+ effect(() => {
544
+ var value = get(tracked);
545
+
546
+ if (value !== input.files && value instanceof FileList) {
547
+ input.files = value;
548
+ }
549
+ });
550
+
551
+ return clear_event;
542
552
  };
543
553
  }
544
554
 
@@ -552,7 +562,7 @@ export function bindNode(maybe_tracked) {
552
562
  throw not_tracked_type_error('bindNode()');
553
563
  }
554
564
 
555
- const tracked = /** @type {Tracked} */ (maybe_tracked);
565
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
556
566
 
557
567
  /** @param {HTMLElement} node */
558
568
  return (node) => {
@@ -369,8 +369,10 @@ export function destroy_block(block, remove_dom = true) {
369
369
 
370
370
  if ((remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0) || (f & HEAD_BLOCK) !== 0) {
371
371
  var s = block.s;
372
- remove_block_dom(s.start, s.end);
373
- removed = true;
372
+ if (s !== null) {
373
+ remove_block_dom(s.start, s.end);
374
+ removed = true;
375
+ }
374
376
  }
375
377
 
376
378
  destroy_block_children(block, remove_dom && !removed);
@@ -284,4 +284,29 @@ describe('basic client > components & composition', () => {
284
284
  render(App);
285
285
  }).not.toThrow();
286
286
  });
287
+
288
+ it('handles component without any output', () => {
289
+ component Noop() {
290
+ // No output
291
+ }
292
+
293
+ component Op() {
294
+ <div>{'Some HTML content'}</div>
295
+ }
296
+
297
+ component App() {
298
+ let Content = track(() => Noop);
299
+ <@Content />
300
+
301
+ <button onClick={() => @Content = Op}>{'Show Op'}</button>
302
+ }
303
+
304
+ render(App);
305
+
306
+ const button = container.querySelector('button');
307
+
308
+ expect(() => {
309
+ button.click();
310
+ }).not.toThrow();
311
+ });
287
312
  });
@@ -81,7 +81,7 @@ describe('composite > props', () => {
81
81
  logs.push(@count);
82
82
  });
83
83
 
84
- <button onClick={() => @count = @count + 1}>{'+'}</button>
84
+ <button onClick={() => (@count = @count + 1)}>{'+'}</button>
85
85
  }
86
86
 
87
87
  component App() {
@@ -122,7 +122,7 @@ describe('composite > props', () => {
122
122
 
123
123
  component Toggle(props) {
124
124
  const [pressed, rest] = trackSplit(props, ['pressed']);
125
- const onClick = () => @pressed = !@pressed;
125
+ const onClick = () => (@pressed = !@pressed);
126
126
  <Button {...@rest} class={@pressed ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
127
127
  <Button class={@pressed ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
128
128
  }