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/'); let classes = {}; 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': 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; return `---@class ${d.name}\n${global ? '' : '__class_'}${d.name} = {}${n}${contents.map(c => transpile(c, indent, `${global ? '' : '__class_'}${d.name}`)).join(n)}\n`; case 'function': return `${d.arguments.map(p => `---@param ${p.name.replace('?', '')} ${p.type}${n}`).join('')}---@return ${d.returns}${n}function ${prefix ? prefix + ':' : ''}${d.name}(${d.arguments.map(a => a.name.replace('?', '')).join(', ')}) end`; case 'const': return `---@type ${d.type}${n}${prefix ? prefix + '.' : ''}${d.name} = nil`; case 'enum': return `${prefix ? prefix + '.' : ''}${d.name} = {${n} ${d.contents.map(c => `${c.name} = ${c.value}`).join(`,${n} `)}${n}}`; } return ''; } /** * @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 code = code.split('\n').filter(c => !c.trim().startsWith('//')).join('\n'); // remove /* */ style comments, for now code = code.replace(/\/\*(.*?)(?=\*\/)/gs, ''); const elements = []; // look for interfaces let interfaceFreeCode = code; const interface = /interface (\w+)(?: extends (\w+))? ?{([^}]*)}/g; while ((match = interface.exec(code)) !== null) { interfaceFreeCode = interfaceFreeCode.replace(match[0], ''); elements.push(['interface', {name: match[1], extends: match[2], contents: parse(match[3])}]); } code = interfaceFreeCode; // look for namespaces let namespaceFreeCode = code; const namespace = /namespace (\w+) ?{([^}]*)}/g; while ((match = namespace.exec(code)) !== null) { namespaceFreeCode = namespaceFreeCode.replace(match[0], ''); elements.push(['namespace', {name: match[1], contents: parse(match[2])}]); } code = namespaceFreeCode; // look for functions let functionFreeCode = code; const functions = /(function)?\s(\w+)\s*\(([^;]*)\)(\s*:\s*([\w\[\]]+))?/g; // wow this sucks while ((match = functions.exec(code)) !== null) { 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) { 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) { constFreeCode = constFreeCode.replace(match[0], ''); elements.push(['const', {name: match[1], type: match[2]}]); } code = constFreeCode; return elements; } (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(); } } let errored = []; let errorReason = {}; const files = await fs.readdir(declarationsPath); let i = 0; for (const f of files) { i++; const filePath = path.resolve(declarationsPath, f); const stat = await fs.lstat(filePath); if (stat.isFile()) { const file = await fs.readFile(filePath, 'utf8'); let startParse = Date.now(); const parsed = parse(file); console.log(` parsed ${chalk.magentaBright(parsed.length)} elements from ${chalk.cyanBright(f)}`); timeParsing += Date.now() - startParse; if (parsed.length === 0) { console.log(`${chalk.redBright('!')} no elements parsed, ${chalk.gray('not writing anything')}`); continue; } 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!`); 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; } console.error(`${chalk.yellowBright('!')} ${err}`); console.error(`${chalk.yellowBright('!')} pushing to end of queue`); files.push(f); errored.push(f); timeTotal += Date.now() - startParse; continue; } console.log(` transpiled w/ final length of ${chalk.magentaBright(transpiled.length + ' chars')}`); timeTotal += Date.now() - startParse; if (transpiled.length === 0) { console.log(`${chalk.redBright('!')} nothing transpiled, ${chalk.gray('not writing anything')}`); continue; } //console.log(inspect(parsed, false, 10)); const luaFilename = f.replace('.d.ts', '.lua'); await fs.writeFile(path.resolve(outPath, luaFilename), '--[[\n' + inspect(parsed, false, null) + '\n]]\n\n' + transpiled); console.log(` wrote ${chalk.cyanBright(luaFilename)}`); } } console.log(`\nfinished transpiling in ${chalk.magentaBright(timeTotal + 'ms')} ${chalk.gray(`(${Math.floor(timeParsing/timeTotal * 1000 + 0.5) / 10}% spent parsing)`)}`); console.log(`check ${chalk.cyanBright(outPath)}`); })();