@@ -368,91 +368,112 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') {
368368
369369 async function walk ( node : ts . Node ) {
370370 const jsdoc = get_jsdoc ( node ) ;
371+
371372 if ( jsdoc ) {
372- for ( const comment of jsdoc ) {
373- let modified = false ;
374-
375- let count = 0 ;
376- for ( const tag of comment . tags ?? [ ] ) {
377- if ( ts . isJSDocTypeTag ( tag ) ) {
378- const [ name , generics ] = await get_type_info ( tag ) ;
379-
380- if ( ts . isFunctionDeclaration ( node ) ) {
381- const is_export = node . modifiers ?. some (
382- ( modifier ) => modifier . kind === ts . SyntaxKind . ExportKeyword
383- )
384- ? 'export '
385- : '' ;
386- const is_async = node . modifiers ?. some (
387- ( modifier ) => modifier . kind === ts . SyntaxKind . AsyncKeyword
388- ) ;
389-
390- const type = generics !== undefined ? `${ name } <${ generics } >` : name ;
391-
392- if ( node . name && node . body ) {
393- code . overwrite (
394- node . getStart ( ) ,
395- node . name . getEnd ( ) ,
396- `${ is_export ? 'export ' : '' } const ${ node . name . getText ( ) } : ${ type } = (${
397- is_async ? 'async ' : ''
398- } `
399- ) ;
400-
401- code . appendLeft ( node . body . getStart ( ) , '=> ' ) ;
402- code . appendLeft ( node . body . getEnd ( ) , ');' ) ;
403-
404- modified = true ;
405- }
406- } else if (
407- ts . isVariableStatement ( node ) &&
408- node . declarationList . declarations . length === 1
409- ) {
410- const variable_statement = node . declarationList . declarations [ 0 ] ;
411-
412- if ( variable_statement . name . getText ( ) === 'actions' ) {
413- let i = variable_statement . getEnd ( ) ;
414- while ( code . original [ i - 1 ] !== '}' ) i -= 1 ;
415- code . appendLeft ( i , ` satisfies ${ name } ` ) ;
416- } else {
417- code . appendLeft (
418- variable_statement . name . getEnd ( ) ,
419- `: ${ name } ${ generics ? `<${ generics } >` : '' } `
420- ) ;
421- }
422-
423- modified = true ;
424- } else {
425- throw new Error ( 'Unhandled @type JsDoc->TS conversion: ' + js_code ) ;
426- }
427- } else if ( ts . isJSDocParameterTag ( tag ) && ts . isFunctionDeclaration ( node ) ) {
428- const sanitised_param = tag
429- . getFullText ( )
430- . replace ( / \s + / g, '' )
431- . replace ( / ( ^ \* | \* $ ) / g, '' ) ;
432-
433- const [ , param_type ] = / @ p a r a m { ( .+ ) } ( .+ ) / . exec ( sanitised_param ) ?? [ ] ;
434-
435- let param_count = 0 ;
436- for ( const param of node . parameters ) {
437- if ( count !== param_count ) {
438- param_count ++ ;
439- continue ;
440- }
441-
442- code . appendLeft ( param . getEnd ( ) , `:${ param_type } ` ) ;
443-
444- param_count ++ ;
445- }
446-
447- modified = true ;
373+ // this isn't an exhaustive list of tags we could potentially encounter (no `@template` etc)
374+ // but it's good enough to cover what's actually in the docs right now
375+ let type : string | null = null ;
376+ let params : string [ ] = [ ] ;
377+ let returns : string | null = null ;
378+ let satisfies : string | null = null ;
379+
380+ if ( jsdoc . length > 1 ) {
381+ throw new Error ( 'woah nelly' ) ;
382+ }
383+
384+ const { comment, tags = [ ] } = jsdoc [ 0 ] ;
385+
386+ for ( const tag of tags ) {
387+ if ( ts . isJSDocTypeTag ( tag ) ) {
388+ type = get_type_info ( tag . typeExpression ) ;
389+ } else if ( ts . isJSDocParameterTag ( tag ) ) {
390+ params . push ( get_type_info ( tag . typeExpression ! ) ) ;
391+ } else if ( ts . isJSDocReturnTag ( tag ) ) {
392+ returns = get_type_info ( tag . typeExpression ! ) ;
393+ } else if ( ts . isJSDocSatisfiesTag ( tag ) ) {
394+ satisfies = get_type_info ( tag . typeExpression ! ) ;
395+ } else {
396+ throw new Error ( 'Unhandled tag' ) ;
397+ }
398+
399+ let start = tag . getStart ( ) ;
400+ let end = tag . getEnd ( ) ;
401+
402+ while ( start > 0 && code . original [ start ] !== '\n' ) start -= 1 ;
403+ while ( end > 0 && code . original [ end ] !== '\n' ) end -= 1 ;
404+ code . remove ( start , end ) ;
405+ }
406+
407+ if ( type && satisfies ) {
408+ throw new Error ( 'Cannot combine @type and @satisfies' ) ;
409+ }
410+
411+ if ( ts . isFunctionDeclaration ( node ) ) {
412+ // convert function to a `const`
413+ if ( type || satisfies ) {
414+ const is_export = node . modifiers ?. some (
415+ ( modifier ) => modifier . kind === ts . SyntaxKind . ExportKeyword
416+ ) ;
417+
418+ const is_async = node . modifiers ?. some (
419+ ( modifier ) => modifier . kind === ts . SyntaxKind . AsyncKeyword
420+ ) ;
421+
422+ code . overwrite (
423+ node . getStart ( ) ,
424+ node . name ! . getStart ( ) ,
425+ is_export ? `export const ` : `const `
426+ ) ;
427+
428+ const modifier = is_async ? 'async ' : '' ;
429+ code . appendLeft (
430+ node . name ! . getEnd ( ) ,
431+ type ? `: ${ type } = ${ modifier } ` : ` = ${ modifier } (`
432+ ) ;
433+
434+ code . prependRight ( node . body ! . getStart ( ) , '=> ' ) ;
435+
436+ code . appendLeft ( node . getEnd ( ) , satisfies ? `) satisfies ${ satisfies } ;` : ';' ) ;
437+ }
438+
439+ for ( let i = 0 ; i < node . parameters . length ; i += 1 ) {
440+ if ( params [ i ] !== undefined ) {
441+ code . appendLeft ( node . parameters [ i ] . getEnd ( ) , `: ${ params [ i ] } ` ) ;
448442 }
443+ }
449444
450- count ++ ;
445+ if ( returns ) {
446+ let start = node . body ! . getStart ( ) ;
447+ while ( code . original [ start - 1 ] !== ')' ) start -= 1 ;
448+ code . appendLeft ( start , `: ${ returns } ` ) ;
451449 }
450+ } else if ( ts . isVariableStatement ( node ) && node . declarationList . declarations . length === 1 ) {
451+ if ( params . length > 0 || returns ) {
452+ throw new Error ( 'TODO handle @params and @returns in variable declarations' ) ;
453+ }
454+
455+ const declaration = node . declarationList . declarations [ 0 ] ;
452456
453- if ( modified ) {
454- code . overwrite ( comment . getStart ( ) , comment . getEnd ( ) , '' ) ;
457+ if ( type ) {
458+ code . appendLeft ( declaration . name . getEnd ( ) , `: ${ type } ` ) ;
455459 }
460+
461+ if ( satisfies ) {
462+ let end = declaration . getEnd ( ) ;
463+ if ( code . original [ end - 1 ] === ';' ) end -= 1 ;
464+ code . appendLeft ( end , ` satisfies ${ satisfies } ` ) ;
465+ }
466+ } else {
467+ throw new Error ( 'Unhandled @type JsDoc->TS conversion: ' + js_code ) ;
468+ }
469+
470+ if ( ! comment ) {
471+ // remove the whole thing
472+ let start = jsdoc [ 0 ] . getStart ( ) ;
473+ let end = jsdoc [ 0 ] . getEnd ( ) ;
474+
475+ while ( start > 0 && code . original [ start ] !== '\n' ) start -= 1 ;
476+ code . overwrite ( start , end , '' ) ;
456477 }
457478 }
458479
@@ -487,42 +508,25 @@ async function convert_to_ts(js_code: string, indent = '', offset = '') {
487508
488509 let transformed = code . toString ( ) ;
489510
490- return transformed === js_code ? undefined : transformed . replace ( / \n \s * \n \s * \n / g , '\n\n' ) ;
491-
492- async function get_type_info ( tag : ts . JSDocTypeTag | ts . JSDocParameterTag ) {
493- const type_text = tag . typeExpression ?. getText ( ) ;
494- let name = type_text ?. slice ( 1 , - 1 ) ; // remove { }
495-
496- const single_line_name = (
497- await prettier . format ( name ?? '' , {
498- printWidth : 1000 ,
499- parser : 'typescript' ,
500- semi : false ,
501- singleQuote : true
502- } )
503- ) . replace ( '\n' , '' ) ;
511+ return transformed === js_code ? undefined : transformed ;
512+
513+ function get_type_info ( expression : ts . JSDocTypeExpression ) {
514+ const type = expression
515+ ?. getText ( ) !
516+ . slice ( 1 , - 1 ) // remove surrounding `{` and `}`
517+ . replace ( / \* ? / gm , '' )
518+ . replace ( / i m p o r t \( ' ( . + ? ) ' \) \. ( \w + ) (?: ( < . + > ) ) ? / gms , ( _ , source , name , args = '' ) => {
519+ const existing = imports . get ( source ) ;
520+ if ( existing ) {
521+ existing . add ( name ) ;
522+ } else {
523+ imports . set ( source , new Set ( [ name ] ) ) ;
524+ }
504525
505- const import_match = / i m p o r t \( ' ( .+ ?) ' \) \. ( \w + ) (?: < ( .+ ) > ) ? $ / s. exec ( single_line_name ) ;
526+ return name + args ;
527+ } ) ;
506528
507- if ( import_match ) {
508- const [ , from , _name , generics ] = import_match ;
509- name = _name ;
510- const existing = imports . get ( from ) ;
511- if ( existing ) {
512- existing . add ( name ) ;
513- } else {
514- imports . set ( from , new Set ( [ name ] ) ) ;
515- }
516- if ( generics !== undefined ) {
517- return [
518- name ,
519- generics
520- . replaceAll ( '*' , '' ) // get rid of JSDoc asterisks
521- . replace ( ' }>' , '}>' ) // unindent closing brace
522- ] ;
523- }
524- }
525- return [ name ] ;
529+ return type ;
526530 }
527531}
528532
@@ -690,7 +694,7 @@ async function syntax_highlight({
690694
691695 // munge shiki output: put whitespace outside `<span>` elements, so that
692696 // highlight delimiters fall outside tokens
693- html = html . replace ( / ( < s p a n [ ^ < ] + ?> ) ( \s + ) / g, '$2$1' ) . replace ( / ( \s + ) ( < \/ s p a n > ) / g, '$2$1' ) ;
697+ html = html . replace ( / ( < s p a n [ ^ > ] + ?> ) ( \s + ) / g, '$2$1' ) . replace ( / ( \s + ) ( < \/ s p a n > ) / g, '$2$1' ) ;
694698
695699 html = html
696700 . replace ( / { 13 } ( [ ^ ] [ ^ ] + ?) { 13 } / g, ( _ , content ) => {
0 commit comments