2021-11-03 05:34:38 +01:00
const fs = require ( 'fs/promises' ) ;
const path = require ( 'path' ) ;
const { inspect } = require ( 'util' ) ;
const chalk = require ( 'chalk' ) ;
const declarationsPath = path . resolve ( _ _dirname , './isaac-typescript-definitions/typings/' ) ;
const outPath = path . resolve ( _ _dirname , './out/' ) ;
2021-11-04 06:35:56 +01:00
const overridePath = path . resolve ( _ _dirname , './override/' ) ;
2021-11-03 05:34:38 +01:00
2021-11-03 07:27:08 +01:00
let classes = { } ;
2021-11-03 09:24:05 +01:00
let tsToLuaTypes = {
undefined : 'nil' ,
unknown : 'any' ,
}
function transpileType ( type ) {
2021-11-03 10:19:28 +01:00
if ( ! type ) return 'what' ;
2021-11-03 09:24:05 +01:00
type = type . trim ( ) ;
if ( type . startsWith ( '(' ) && type . endsWith ( ')' ) ) type = type . slice ( 1 , - 1 ) ;
if ( tsToLuaTypes [ type ] ) {
return tsToLuaTypes [ type ] ;
}
if ( type . endsWith ( '[]' ) ) {
return transpileType ( type . slice ( 0 , - 2 ) ) + '[]' ;
}
2021-11-04 07:15:10 +01:00
if ( type . startsWith ( 'LuaMultiReturn<[' ) && type . endsWith ( ']>' ) ) {
return type . replace ( 'LuaMultiReturn<[' , '' ) . replace ( ']>' , '' ) . split ( ',' ) . map ( t => transpileType ( t . split ( ':' ) [ 1 ] ) ) . join ( ', ' ) ;
}
2021-11-03 09:24:05 +01:00
if ( type . includes ( '|' ) ) {
return type . split ( '|' ) . map ( t => transpileType ( t ) ) . join ( ' | ' ) ;
}
2021-11-03 10:19:28 +01:00
if ( type . startsWith ( '"' ) ) {
return 'string' ;
}
if ( type . startsWith ( 'Record<' ) && type . endsWith ( '>' ) ) {
2021-11-03 09:24:05 +01:00
return 'table<' + transpileType ( type . split ( ',' ) [ 0 ] . replace ( 'Record<' , '' ) ) + ', ' + transpileType ( type . split ( ',' ) [ 1 ] . replace ( '>' , '' ) ) + '>' ;
}
2021-11-03 10:19:28 +01:00
if ( type . startsWith ( 'LuaTable<' ) && type . endsWith ( '>' ) ) {
2021-11-03 09:24:05 +01:00
return 'table<' + transpileType ( type . split ( ',' ) [ 0 ] . replace ( 'LuaTable<' , '' ) ) + ', ' + transpileType ( type . split ( ',' ) [ 1 ] . replace ( '>' , '' ) ) + '>' ;
}
let match ;
if ( match = /\((.*?)\) ?=> ?(.*)/ . exec ( type ) ) { // functions
return ` fun( ${ match [ 1 ] !== '' ? match [ 1 ] . split ( ',' ) . map ( c => c . split ( ':' ) [ 0 ] + ':' + transpileType ( c . split ( ':' ) [ 1 ] ) ) . join ( ',' ) : '' } ): ${ transpileType ( match [ 2 ] ) } ` ;
}
if ( match = /(?:Array)|(?:Map)<(.*?)>/ . exec ( type ) ) {
return transpileType ( match [ 1 ] ) + '[]' ;
}
if ( match = /(.*?)<(.*?)>/ . exec ( type ) ) { // any other weird type we just discard
return transpileType ( match [ 2 ] ) ;
}
return type ;
}
2021-11-03 05:34:38 +01:00
function transpile ( parsed , indent , prefix ) {
let global = false ;
indent = indent || 0 ;
const type = parsed [ 0 ] ;
const d = parsed [ 1 ] ;
let n = '\n' + ' ' . repeat ( indent ) ; // newline
switch ( type ) {
case 'namespace' :
global = true ;
case 'interface' :
2021-11-03 07:27:08 +01:00
let contents = d . contents ;
if ( d . extends ) {
if ( classes [ d . extends ] ) {
contents . push ( ... classes [ d . extends ] ) ;
} else {
throw new Error ( ` Class not found in class storage: ${ d . extends } ` )
}
}
classes [ d . name ] = contents ;
2021-11-04 05:45:08 +01:00
return ` ---@class ${ d . name } ${ d . extends ? ' : ' + d . extends : '' } ${ n } ${ contents . filter ( e => e [ 0 ] === 'const' ) . map ( c => ` ---@field public ${ c [ 1 ] . name } ${ transpileType ( c [ 1 ] . type ) } ${ n } ` ) . join ( '' ) } ${ global ? '' : '__class_' } ${ d . name } = {} ${ n } ${ contents . filter ( e => e [ 0 ] !== 'const' ) . map ( c => transpile ( c , indent , ` ${ global ? '' : '__class_' } ${ d . name } ` ) ) . join ( n ) } \n ` ;
2021-11-03 05:34:38 +01:00
case 'function' :
2021-11-03 09:24:05 +01:00
return ` ${ d . arguments . map ( p => ` ---@param ${ p . name . replace ( '?' , '' ) } ${ transpileType ( p . type ) } ${ n } ` ) . join ( '' ) } ---@return ${ transpileType ( d . returns ) } ${ n } function ${ prefix ? prefix + ':' : '' } ${ d . name } ( ${ d . arguments . map ( a => a . name . replace ( '?' , '' ) ) . join ( ', ' ) } ) end ` ;
2021-11-03 05:34:38 +01:00
case 'const' :
2021-11-03 09:24:05 +01:00
return ` ---@type ${ transpileType ( d . type ) } ${ n } ${ prefix ? prefix + '.' : '' } ${ d . name } = nil ` ;
2021-11-03 05:34:38 +01:00
case 'enum' :
return ` ${ prefix ? prefix + '.' : '' } ${ d . name } = { ${ n } ${ d . contents . map ( c => ` ${ c . name } = ${ c . value } ` ) . join ( ` , ${ n } ` ) } ${ n } } ` ;
}
return '' ;
}
2021-11-03 08:40:28 +01:00
function removeComments ( code ) {
// remove /* */ style comments, for now
code = code . replace ( / \ / \ * ( . * ? ) \ * \ / / g s , ' ' ) ;
code = code . split ( '\n' ) . filter ( c => ! c . trim ( ) . startsWith ( '//' ) ) . join ( '\n' ) ;
return code ;
}
2021-11-03 09:37:52 +01:00
let parsedElements = 0 ;
2021-11-03 05:34:38 +01:00
/ * *
* @ param { string } code
* /
function parse ( code ) {
// console.log(code);
// we're left with code containing "declare" statements
// other code will pass stuff in here recursively, but the declare doesnt _really_ matter
// were gonna end up with an ast of sorts anyways as a result, which we can then use instead of declares
// sanitize stuff
2021-11-03 08:40:28 +01:00
code = removeComments ( code ) ;
2021-11-03 05:34:38 +01:00
2021-11-03 09:24:05 +01:00
let match ;
2021-11-03 10:53:40 +01:00
let elements = [ ] ;
2021-11-03 05:34:38 +01:00
// look for interfaces
let interfaceFreeCode = code ;
2021-11-03 07:27:08 +01:00
const interface = /interface (\w+)(?: extends (\w+))? ?{([^}]*)}/g ;
2021-11-03 05:34:38 +01:00
while ( ( match = interface . exec ( code ) ) !== null ) {
2021-11-03 09:37:52 +01:00
parsedElements ++ ;
2021-11-03 05:34:38 +01:00
interfaceFreeCode = interfaceFreeCode . replace ( match [ 0 ] , '' ) ;
2021-11-03 07:27:08 +01:00
elements . push ( [ 'interface' , { name : match [ 1 ] , extends : match [ 2 ] , contents : parse ( match [ 3 ] ) } ] ) ;
2021-11-03 05:34:38 +01:00
}
code = interfaceFreeCode ;
// look for namespaces
let namespaceFreeCode = code ;
const namespace = /namespace (\w+) ?{([^}]*)}/g ;
while ( ( match = namespace . exec ( code ) ) !== null ) {
2021-11-03 09:37:52 +01:00
parsedElements ++ ;
2021-11-03 05:34:38 +01:00
namespaceFreeCode = namespaceFreeCode . replace ( match [ 0 ] , '' ) ;
elements . push ( [ 'namespace' , { name : match [ 1 ] , contents : parse ( match [ 2 ] ) } ] ) ;
}
code = namespaceFreeCode ;
// look for functions
let functionFreeCode = code ;
2021-11-04 07:15:10 +01:00
const functions = /(function)?\s(\w+)\s*\(([^;]*)\)(\s*:\s*([\w\[\]\s|<>,:]+))?/g ; // wow this sucks
2021-11-03 05:34:38 +01:00
while ( ( match = functions . exec ( code ) ) !== null ) {
2021-11-03 09:37:52 +01:00
parsedElements ++ ;
2021-11-03 05:34:38 +01:00
functionFreeCode = functionFreeCode . replace ( match [ 0 ] , '' ) ;
let arguments = match [ 3 ] . split ( ',' ) . map ( s => {
const split = s . split ( ':' ) . map ( s => s . trim ( ) ) ;
return { type : split [ 1 ] , name : split [ 0 ] }
} ) . filter ( c => c . type && c . name ) ;
elements . push ( [ 'function' , { name : match [ 2 ] , arguments : arguments , returns : match [ 5 ] } ] ) ;
}
code = functionFreeCode ;
// looks for enums
let enumFreeCode = code ;
const enums = /enum (\w+) ?{([^}]*)}/g ;
while ( ( match = enums . exec ( code ) ) !== null ) {
2021-11-03 09:37:52 +01:00
parsedElements ++ ;
2021-11-03 05:34:38 +01:00
enumFreeCode = enumFreeCode . replace ( match [ 0 ] , '' ) ;
elements . push ( [ 'enum' , { name : match [ 1 ] , contents : match [ 2 ] . split ( ',' ) . filter ( c => c . split ( '=' ) . length === 2 ) . map ( c => { return { name : c . split ( '=' ) [ 0 ] . trim ( ) , value : c . split ( '=' ) [ 1 ] . trim ( ) } } ) } ] ) ;
}
code = enumFreeCode ;
// looks for constants / properties
let constFreeCode = code ;
const consts = /(\w+)\s*:\s*(\w+)/g ;
while ( ( match = consts . exec ( code ) ) !== null ) {
2021-11-03 09:37:52 +01:00
parsedElements ++ ;
2021-11-03 05:34:38 +01:00
constFreeCode = constFreeCode . replace ( match [ 0 ] , '' ) ;
elements . push ( [ 'const' , { name : match [ 1 ] , type : match [ 2 ] } ] ) ;
}
code = constFreeCode ;
2021-11-03 10:53:40 +01:00
elements = elements . sort ( ( a , b ) => b [ 1 ] . extends ? 1 : 0 - a [ 1 ] . extends ? 1 : 0 ) . reverse ( ) ; // for extending bullshit
2021-11-03 05:34:38 +01:00
return elements ;
}
2021-11-04 07:19:28 +01:00
async function recursiveReaddir ( rpath , p ) {
2021-11-03 09:58:21 +01:00
p = p || '' ;
2021-11-04 07:19:28 +01:00
let fileNames = [ ] ;
2021-11-03 09:58:21 +01:00
const files = await fs . readdir ( rpath ) ;
for ( const f of files ) {
const stat = await fs . lstat ( path . join ( rpath , f ) ) ;
if ( stat . isDirectory ( ) || stat . isSymbolicLink ( ) ) {
2021-11-04 07:19:28 +01:00
fileNames . push ( ... await recursiveReaddir ( path . join ( rpath , f ) , p + f + '/' ) ) ;
2021-11-03 09:58:21 +01:00
} else {
fileNames . push ( p + f ) ;
}
}
return fileNames ;
}
2021-11-03 05:34:38 +01:00
( async ( ) => {
let timeParsing = 0 ;
let timeTotal = 0 ;
try {
const d = await fs . lstat ( outPath )
if ( ! d . isDirectory ( ) ) throw new Error ( ` ${ outPath } exists, and isn't a directory! ` ) ;
} catch ( err ) {
if ( err . code === 'ENOENT' ) {
await fs . mkdir ( outPath ) ;
} else {
console . error ( err ) ;
process . exit ( ) ;
}
}
2021-11-03 07:27:08 +01:00
let errored = [ ] ;
let errorReason = { } ;
2021-11-03 09:58:21 +01:00
const files = await recursiveReaddir ( declarationsPath ) ;
2021-11-03 09:37:52 +01:00
const filesAmt = files . length ;
2021-11-03 07:27:08 +01:00
let i = 0 ;
2021-11-03 05:34:38 +01:00
for ( const f of files ) {
2021-11-03 07:27:08 +01:00
i ++ ;
2021-11-03 05:34:38 +01:00
const filePath = path . resolve ( declarationsPath , f ) ;
2021-11-03 09:58:21 +01:00
console . log ( ` ${ chalk . cyanBright ( f ) } ` ) ;
2021-11-04 06:35:56 +01:00
const override = path . resolve ( overridePath , f . replace ( '.d.ts' , '.lua' ) ) ;
const luaFilename = f . replace ( '.d.ts' , '.lua' ) . split ( '/' ) . join ( '_' ) ;
try {
if ( await fs . stat ( override ) ) {
console . log ( ' file exists in override/, ignoring' ) ;
await fs . copyFile ( override , path . resolve ( outPath , luaFilename ) ) ;
continue ;
}
} catch ( err ) { }
2021-11-03 09:58:21 +01:00
const file = await fs . readFile ( filePath , 'utf8' ) ;
2021-11-03 05:34:38 +01:00
2021-11-03 09:58:21 +01:00
let startParse = Date . now ( ) ;
parsedElements = 0 ;
const parsed = parse ( file ) ;
console . log ( ` parsed ${ chalk . magentaBright ( parsedElements ) } objects from ${ chalk . cyanBright ( f ) } ` ) ;
timeParsing += Date . now ( ) - startParse ;
2021-11-03 05:34:38 +01:00
2021-11-03 09:58:21 +01:00
if ( parsed . length === 0 ) {
console . log ( ` ${ chalk . redBright ( '!' ) } no elements parsed, ${ chalk . gray ( 'not writing anything' ) } ` ) ;
continue ;
}
if ( parsed . length > 10 ) {
console . log ( ` ${ chalk . redBright ( '!' ) } over 10 top-level objects were parsed - this may be a bad idea ` ) ;
}
2021-11-03 07:27:08 +01:00
2021-11-03 09:58:21 +01:00
let transpiled ;
try {
transpiled = parsed . map ( p => transpile ( p ) ) . join ( '\n' ) . trim ( ) ;
errored = errored . filter ( e => e !== f ) ;
} catch ( err ) {
errorReason [ f ] = err ;
if ( errored . length > files . length - i ) {
console . error ( ` ${ chalk . redBright ( '!' ) } failed resolving extends! ` ) ;
2021-11-03 10:19:28 +01:00
errored = [ ... new Set ( errored ) ] ;
2021-11-03 09:58:21 +01:00
console . error ( ` ${ chalk . redBright ( '!' ) } abandoned ${ chalk . magentaBright ( errored . length ) } files: ` ) ;
let longest = errored . reduce ( ( a , b ) => Math . max ( a . length || a || 0 , b . length ) ) ;
errored . forEach ( e => console . error ( ` - ${ chalk . cyanBright ( e ) } ${ ' ' . repeat ( longest - e . length + 2 ) } ${ chalk . gray ( ' → ' + errorReason [ e ] . message . replace ( 'Class not found in class storage: ' , '' ) ) } ` ) ) ;
break ;
}
2021-11-03 07:27:08 +01:00
2021-11-03 09:58:21 +01:00
console . error ( ` ${ chalk . yellowBright ( '!' ) } ${ err } ` ) ;
console . error ( ` ${ chalk . yellowBright ( '!' ) } pushing to end of queue ` ) ;
files . push ( f ) ;
errored . push ( f ) ;
2021-11-03 07:27:08 +01:00
2021-11-03 05:34:38 +01:00
timeTotal += Date . now ( ) - startParse ;
2021-11-03 09:58:21 +01:00
continue ;
}
console . log ( ` transpiled w/ final length of ${ chalk . magentaBright ( transpiled . length + ' chars' ) } ` ) ;
timeTotal += Date . now ( ) - startParse ;
2021-11-03 05:34:38 +01:00
2021-11-03 09:58:21 +01:00
if ( transpiled . length === 0 ) {
console . log ( ` ${ chalk . redBright ( '!' ) } nothing transpiled, ${ chalk . gray ( 'not writing anything' ) } ` ) ;
continue ;
2021-11-03 05:34:38 +01:00
}
2021-11-03 09:58:21 +01:00
//console.log(inspect(parsed, false, 10));
await fs . writeFile ( path . resolve ( outPath , luaFilename ) , transpiled ) ;
2021-11-03 10:54:51 +01:00
//console.log(` wrote ${chalk.cyanBright(luaFilename)}`);
2021-11-03 05:34:38 +01:00
}
2021-11-04 06:09:28 +01:00
console . log ( ` \n finished transpiling ${ chalk . magentaBright ( filesAmt ) } ${ chalk . gray ( ` + ${ files . length - filesAmt } ` ) } files in ${ chalk . magentaBright ( timeTotal + 'ms' ) } avg ${ chalk . magentaBright ( ( Math . floor ( timeTotal / filesAmt * 100 ) / 100 ) + 'ms' ) } ${ chalk . gray ( ` ( ${ Math . floor ( timeParsing / timeTotal * 1000 + 0.5 ) / 10 } % spent parsing) ` ) } ` ) ;
console . log ( ` total class cache size: ${ chalk . magentaBright ( Object . keys ( classes ) . length + ' classes' ) } ` )
2021-11-03 05:34:38 +01:00
console . log ( ` check ${ chalk . cyanBright ( outPath ) } ` ) ;
} ) ( ) ;