@@ -4,6 +4,7 @@ import { internalHelpers } from '../../helpers';
4
4
import { surroundWithIgnoreComments } from '../../utils/ignore' ;
5
5
import { preprendStr , overwriteStr } from '../../utils/magic-string' ;
6
6
import { findExportKeyword , getLastLeadingDoc , isInterfaceOrTypeDeclaration } from '../utils/tsAst' ;
7
+ import { HoistableInterfaces } from './HoistableInterfaces' ;
7
8
8
9
export function is$$PropsDeclaration (
9
10
node : ts . Node
@@ -21,6 +22,7 @@ interface ExportedName {
21
22
}
22
23
23
24
export class ExportedNames {
25
+ public hoistableInterfaces = new HoistableInterfaces ( ) ;
24
26
public usesAccessors = false ;
25
27
/**
26
28
* Uses the `$$Props` type
@@ -35,7 +37,9 @@ export class ExportedNames {
35
37
* If using TS, this returns the generic string, if using JS, returns the `@type {..}` string.
36
38
*/
37
39
private $props = {
40
+ /** The JSDoc type; not set when TS type exists */
38
41
comment : '' ,
42
+ /** The TS type */
39
43
type : '' ,
40
44
bindings : [ ] as string [ ]
41
45
} ;
@@ -173,10 +177,13 @@ export class ExportedNames {
173
177
}
174
178
}
175
179
180
+ // Easy mode: User uses TypeScript and typed the $props() rune
176
181
if ( node . initializer . typeArguments ?. length > 0 || node . type ) {
182
+ this . hoistableInterfaces . analyze$propsRune ( node ) ;
183
+
177
184
const generic_arg = node . initializer . typeArguments ?. [ 0 ] || node . type ;
178
185
const generic = generic_arg . getText ( ) ;
179
- if ( ! generic . includes ( '{' ) ) {
186
+ if ( ts . isTypeReferenceNode ( generic_arg ) ) {
180
187
this . $props . type = generic ;
181
188
} else {
182
189
// Create a virtual type alias for the unnamed generic and reuse it for the props return type
@@ -199,145 +206,148 @@ export class ExportedNames {
199
206
surroundWithIgnoreComments ( this . $props . type )
200
207
) ;
201
208
}
202
- } else {
203
- if ( ! this . isTsFile ) {
204
- const text = node . getSourceFile ( ) . getFullText ( ) ;
205
- let start = - 1 ;
206
- let comment : string ;
207
- // reverse because we want to look at the last comment before the node first
208
- for ( const c of [ ...( ts . getLeadingCommentRanges ( text , node . pos ) || [ ] ) ] . reverse ( ) ) {
209
+
210
+ return ;
211
+ }
212
+
213
+ // Hard mode: User uses JSDoc or didn't type the $props() rune
214
+ if ( ! this . isTsFile ) {
215
+ const text = node . getSourceFile ( ) . getFullText ( ) ;
216
+ let start = - 1 ;
217
+ let comment : string ;
218
+ // reverse because we want to look at the last comment before the node first
219
+ for ( const c of [ ...( ts . getLeadingCommentRanges ( text , node . pos ) || [ ] ) ] . reverse ( ) ) {
220
+ const potential_match = text . substring ( c . pos , c . end ) ;
221
+ if ( / @ t y p e \b / . test ( potential_match ) ) {
222
+ comment = potential_match ;
223
+ start = c . pos + this . astOffset ;
224
+ break ;
225
+ }
226
+ }
227
+ if ( ! comment ) {
228
+ for ( const c of [
229
+ ...( ts . getLeadingCommentRanges ( text , node . parent . pos ) || [ ] ) . reverse ( )
230
+ ] ) {
209
231
const potential_match = text . substring ( c . pos , c . end ) ;
210
232
if ( / @ t y p e \b / . test ( potential_match ) ) {
211
233
comment = potential_match ;
212
234
start = c . pos + this . astOffset ;
213
235
break ;
214
236
}
215
237
}
216
- if ( ! comment ) {
217
- for ( const c of [
218
- ...( ts . getLeadingCommentRanges ( text , node . parent . pos ) || [ ] ) . reverse ( )
219
- ] ) {
220
- const potential_match = text . substring ( c . pos , c . end ) ;
221
- if ( / @ t y p e \b / . test ( potential_match ) ) {
222
- comment = potential_match ;
223
- start = c . pos + this . astOffset ;
224
- break ;
225
- }
226
- }
227
- }
228
-
229
- if ( comment && / \/ \* \* [ ^ @ ] * ?@ t y p e \s * { \s * { .* } \s * } \s * \* \/ / . test ( comment ) ) {
230
- // Create a virtual type alias for the unnamed generic and reuse it for the props return type
231
- // so that rename, find references etc works seamlessly across components
232
- this . $props . comment = '/** @type {$$ComponentProps} */' ;
233
- const type_start = this . str . original . indexOf ( '@type' , start ) ;
234
- this . str . overwrite ( type_start , type_start + 5 , '@typedef' ) ;
235
- const end = this . str . original . indexOf ( '*/' , start ) ;
236
- this . str . overwrite ( end , end + 2 , ' $$ComponentProps */' + this . $props . comment ) ;
237
- } else {
238
- // Complex comment or simple `@type {AType}` comment which we just use as-is.
239
- // For the former this means things like rename won't work properly across components.
240
- this . $props . comment = comment || '' ;
241
- }
242
238
}
243
239
244
- if ( this . $props . comment ) {
245
- return ;
240
+ if ( comment && / \/ \* \* [ ^ @ ] * ?@ t y p e \s * { \s * { .* } \s * } \s * \* \/ / . test ( comment ) ) {
241
+ // Create a virtual type alias for the unnamed generic and reuse it for the props return type
242
+ // so that rename, find references etc works seamlessly across components
243
+ this . $props . comment = '/** @type {$$ComponentProps} */' ;
244
+ const type_start = this . str . original . indexOf ( '@type' , start ) ;
245
+ this . str . overwrite ( type_start , type_start + 5 , '@typedef' ) ;
246
+ const end = this . str . original . indexOf ( '*/' , start ) ;
247
+ this . str . overwrite ( end , end + 2 , ' $$ComponentProps */' + this . $props . comment ) ;
248
+ } else {
249
+ // Complex comment or simple `@type {AType}` comment which we just use as-is.
250
+ // For the former this means things like rename won't work properly across components.
251
+ this . $props . comment = comment || '' ;
246
252
}
253
+ }
247
254
248
- // Do a best-effort to extract the props from the object literal
249
- let propsStr = '' ;
250
- let withUnknown = false ;
251
- let props = [ ] ;
252
-
253
- const isKitRouteFile = internalHelpers . isKitRouteFile ( this . basename ) ;
254
- const isKitLayoutFile = isKitRouteFile && this . basename . includes ( 'layout' ) ;
255
-
256
- if ( ts . isObjectBindingPattern ( node . name ) ) {
257
- for ( const element of node . name . elements ) {
258
- if (
259
- ! ts . isIdentifier ( element . name ) ||
260
- ( element . propertyName && ! ts . isIdentifier ( element . propertyName ) ) ||
261
- ! ! element . dotDotDotToken
262
- ) {
263
- withUnknown = true ;
264
- } else {
265
- const name = element . propertyName
266
- ? ( element . propertyName as ts . Identifier ) . text
267
- : element . name . text ;
268
- if ( isKitRouteFile ) {
269
- if ( name === 'data' ) {
270
- props . push (
271
- `data: import('./$types.js').${
272
- isKitLayoutFile ? 'LayoutData' : 'PageData'
273
- } `
274
- ) ;
275
- }
276
- if ( name === 'form' && ! isKitLayoutFile ) {
277
- props . push ( `form: import('./$types.js').ActionData` ) ;
278
- }
279
- } else if ( element . initializer ) {
280
- const type = ts . isAsExpression ( element . initializer )
281
- ? element . initializer . type . getText ( )
282
- : ts . isStringLiteral ( element . initializer )
283
- ? 'string'
284
- : ts . isNumericLiteral ( element . initializer )
285
- ? 'number'
286
- : element . initializer . kind === ts . SyntaxKind . TrueKeyword ||
287
- element . initializer . kind === ts . SyntaxKind . FalseKeyword
288
- ? 'boolean'
289
- : ts . isIdentifier ( element . initializer )
290
- ? `typeof ${ element . initializer . text } `
291
- : ts . isObjectLiteralExpression ( element . initializer )
292
- ? 'Record<string, unknown>'
293
- : ts . isArrayLiteralExpression ( element . initializer )
294
- ? 'unknown[]'
295
- : 'unknown' ;
296
- props . push ( `${ name } ?: ${ type } ` ) ;
297
- } else {
298
- props . push ( `${ name } : unknown` ) ;
255
+ if ( this . $props . comment ) {
256
+ // User uses JsDoc
257
+ return ;
258
+ }
259
+
260
+ // Do a best-effort to extract the props from the object literal
261
+ let propsStr = '' ;
262
+ let withUnknown = false ;
263
+ let props = [ ] ;
264
+
265
+ const isKitRouteFile = internalHelpers . isKitRouteFile ( this . basename ) ;
266
+ const isKitLayoutFile = isKitRouteFile && this . basename . includes ( 'layout' ) ;
267
+
268
+ if ( ts . isObjectBindingPattern ( node . name ) ) {
269
+ for ( const element of node . name . elements ) {
270
+ if (
271
+ ! ts . isIdentifier ( element . name ) ||
272
+ ( element . propertyName && ! ts . isIdentifier ( element . propertyName ) ) ||
273
+ ! ! element . dotDotDotToken
274
+ ) {
275
+ withUnknown = true ;
276
+ } else {
277
+ const name = element . propertyName
278
+ ? ( element . propertyName as ts . Identifier ) . text
279
+ : element . name . text ;
280
+ if ( isKitRouteFile ) {
281
+ if ( name === 'data' ) {
282
+ props . push (
283
+ `data: import('./$types.js').${
284
+ isKitLayoutFile ? 'LayoutData' : 'PageData'
285
+ } `
286
+ ) ;
299
287
}
288
+ if ( name === 'form' && ! isKitLayoutFile ) {
289
+ props . push ( `form: import('./$types.js').ActionData` ) ;
290
+ }
291
+ } else if ( element . initializer ) {
292
+ const type = ts . isAsExpression ( element . initializer )
293
+ ? element . initializer . type . getText ( )
294
+ : ts . isStringLiteral ( element . initializer )
295
+ ? 'string'
296
+ : ts . isNumericLiteral ( element . initializer )
297
+ ? 'number'
298
+ : element . initializer . kind === ts . SyntaxKind . TrueKeyword ||
299
+ element . initializer . kind === ts . SyntaxKind . FalseKeyword
300
+ ? 'boolean'
301
+ : ts . isIdentifier ( element . initializer )
302
+ ? `typeof ${ element . initializer . text } `
303
+ : ts . isObjectLiteralExpression ( element . initializer )
304
+ ? 'Record<string, unknown>'
305
+ : ts . isArrayLiteralExpression ( element . initializer )
306
+ ? 'unknown[]'
307
+ : 'unknown' ;
308
+ props . push ( `${ name } ?: ${ type } ` ) ;
309
+ } else {
310
+ props . push ( `${ name } : unknown` ) ;
300
311
}
301
312
}
313
+ }
302
314
303
- if ( isKitLayoutFile ) {
304
- props . push ( `children: import('svelte').Snippet` ) ;
305
- }
315
+ if ( isKitLayoutFile ) {
316
+ props . push ( `children: import('svelte').Snippet` ) ;
317
+ }
306
318
307
- if ( props . length > 0 ) {
308
- propsStr =
309
- `{ ${ props . join ( ', ' ) } }` +
310
- ( withUnknown ? ' & Record<string, unknown>' : '' ) ;
311
- } else if ( withUnknown ) {
312
- propsStr = 'Record<string, unknown>' ;
313
- } else {
314
- propsStr = 'Record<string, never>' ;
315
- }
316
- } else {
319
+ if ( props . length > 0 ) {
320
+ propsStr =
321
+ `{ ${ props . join ( ', ' ) } }` + ( withUnknown ? ' & Record<string, unknown>' : '' ) ;
322
+ } else if ( withUnknown ) {
317
323
propsStr = 'Record<string, unknown>' ;
324
+ } else {
325
+ propsStr = 'Record<string, never>' ;
318
326
}
327
+ } else {
328
+ propsStr = 'Record<string, unknown>' ;
329
+ }
319
330
320
- // Create a virtual type alias for the unnamed generic and reuse it for the props return type
321
- // so that rename, find references etc works seamlessly across components
322
- if ( this . isTsFile ) {
323
- this . $props . type = '$$ComponentProps' ;
324
- if ( props . length > 0 || withUnknown ) {
325
- preprendStr (
326
- this . str ,
327
- node . parent . pos + this . astOffset ,
328
- surroundWithIgnoreComments ( `;type $$ComponentProps = ${ propsStr } ;` )
329
- ) ;
330
- preprendStr ( this . str , node . name . end + this . astOffset , `: ${ this . $props . type } ` ) ;
331
- }
332
- } else {
333
- this . $props . comment = '/** @type {$$ComponentProps} */' ;
334
- if ( props . length > 0 || withUnknown ) {
335
- preprendStr (
336
- this . str ,
337
- node . pos + this . astOffset ,
338
- `/** @typedef {${ propsStr } } $$ComponentProps */${ this . $props . comment } `
339
- ) ;
340
- }
331
+ // Create a virtual type alias for the unnamed generic and reuse it for the props return type
332
+ // so that rename, find references etc works seamlessly across components
333
+ if ( this . isTsFile ) {
334
+ this . $props . type = '$$ComponentProps' ;
335
+ if ( props . length > 0 || withUnknown ) {
336
+ preprendStr (
337
+ this . str ,
338
+ node . parent . pos + this . astOffset ,
339
+ surroundWithIgnoreComments ( `;type $$ComponentProps = ${ propsStr } ;` )
340
+ ) ;
341
+ preprendStr ( this . str , node . name . end + this . astOffset , `: ${ this . $props . type } ` ) ;
342
+ }
343
+ } else {
344
+ this . $props . comment = '/** @type {$$ComponentProps} */' ;
345
+ if ( props . length > 0 || withUnknown ) {
346
+ preprendStr (
347
+ this . str ,
348
+ node . pos + this . astOffset ,
349
+ `/** @typedef {${ propsStr } } $$ComponentProps */${ this . $props . comment } `
350
+ ) ;
341
351
}
342
352
}
343
353
}
0 commit comments