isaac-lua-definitions/index.js

401 lines
14 KiB
JavaScript
Raw Normal View History

2021-11-03 05:34:38 +01:00
const fs = require('fs/promises');
const path = require('path');
const chalk = require('chalk');
const { program } = require('commander');
2021-11-03 05:34:38 +01:00
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
program
.name('isaac-lua-definitions-transpiler')
.version('0.0.1')
.option('-W, --no-warn', 'don\'t show warnings')
.option('-s', 'don\'t show file info')
.option('-ss', 'don\'t show files')
.option('-sss', 'don\'t show anything')
.option('-v', 'show class names')
.option('-e, --exclude [files...]', 'don\'t compile specified files', '')
.option('-o, --only [files...]', 'only compile specified files', '');
program.parse(process.argv);
const options = program.opts();
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(']>')) {
2021-11-10 12:09:30 +01:00
return type.replace('LuaMultiReturn<[','').replace(']>','').split(',').map(t => transpileType(t.split(':')[1] || t)).join(', ');
2021-11-04 07:15:10 +01:00
}
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;
}
function transpile(parsed, indent, prefix, gl) {
2021-11-03 05:34:38 +01:00
let global = false;
indent = indent || 0;
const type = parsed[0];
const d = parsed[1];
let n = '\n' + ' '.repeat(indent); // newline
const comments = d.comment ? d.comment.split('\n').map(l => l.trim()).map(l => l.startsWith('*') ? l.replace('*', '') : l).filter(l => l.trim().length !== 0 && !l.trim().startsWith('@')).map(l => '---' + l.trim()).join('\n') + '\n' : '';
2021-11-03 05:34:38 +01:00
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;
2022-05-08 23:06:22 +02:00
return `${comments}---@class ${d.name}${d.extends ? ' : ' + d.extends : ''}${n}${contents.filter(e => e[0] === 'const').map(c => `---@field ${c[1].type.startsWith('Readonly<') ? 'protected' : '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}`, global)).join(n)}\n`;
2021-11-03 05:34:38 +01:00
case 'function':
if (d.arguments[0] && d.arguments[0].type === 'void') d.arguments = d.arguments.slice(1);
2022-01-14 11:08:39 +01:00
return `${comments}${d.arguments.map(p => `---@param ${p.name.replace('?', '')} ${transpileType(p.type)}${p.comment ? ' @' + p.comment : ''}${n}`).join('')}---@return ${transpileType(d.returns)}${d.returnsComment ? ' @' + d.returnsComment : ''}${n}function ${prefix ? prefix + (gl ? '.' : ':') : ''}${d.name}(${d.arguments.map(a => a.name.replace('?', '')).join(', ')}) end`;
2021-11-03 05:34:38 +01:00
case 'const':
return `${comments}---@type ${transpileType(d.type)}${n}${prefix ? prefix + '.' : ''}${d.name} = nil`;
2021-11-03 05:34:38 +01:00
case 'enum':
return `${comments}${prefix ? prefix + '.' : ''}${d.name} = {${n} ${d.contents.map(c => `${c.name} = ${c.value}`).join(`,${n} `)}${n}}`;
2021-11-03 05:34:38 +01:00
}
return '';
}
2021-11-03 08:40:28 +01:00
function removeComments(code) {
// remove /* */ style comments, for now
code = code.replace(/\/\*(.*?)\*\//gs, '');
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, originalCode) {
2021-11-03 05:34:38 +01:00
// 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
if (!originalCode) originalCode = code;
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], '');
if (options.v) log(undefined, ' interface ' + chalk.blueBright(match[1]));
elements.push(['interface', {name: match[1], extends: match[2], contents: parse(match[3], originalCode)}]);
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], '');
if (options.v) log(undefined, ' namespace ' + chalk.redBright(match[1]));
elements.push(['namespace', {name: match[1], contents: parse(match[2], originalCode)}]);
2021-11-03 05:34:38 +01:00
}
code = namespaceFreeCode;
// look for functions
let functionFreeCode = code;
2022-05-08 23:06:22 +02: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);
const origIndex = originalCode.indexOf(match[0]);
const comment = /\/\*(.*?)\*\//gs;
let comments = [];
let comm;
while ((comm = comment.exec(originalCode.slice(0, origIndex))) !== null) {
comments.push([comm[1], comm.index + comm[0].length]);
}
comments = comments.filter(c => Math.abs(origIndex - c[1]) < 10);
const c = comments.pop();
//if (c) c[0] += 'isaac-lua-definitions debug: comment distance = ' + (origIndex - c[1]).toString();
2022-01-14 11:08:39 +01:00
let returnsComment;
2022-01-14 10:56:07 +01:00
// add parameter comments
if (c) {
c[0].split('\n').map(l => l.trim()).filter(l => l.startsWith('* @param ')).forEach(p => {
p = p.slice('* @param '.length);
let indx = arguments.findIndex(arg => arg.name.replace('?', '').trim() === p.split(' ')[0]);
if (p.split(' ')[1] && indx !== -1) {
let comment = p.split(' ').slice(1).join(' ');
arguments[indx].comment = comment;
}
2022-01-14 11:08:39 +01:00
});
// add returns comments, if found
let returns = c[0].split('\n').map(l => l.trim()).find(l => l.startsWith('* @returns '));
if (returns) {
returnsComment = returns.slice('* @returns '.length);
}
2022-01-14 10:56:07 +01:00
}
2022-01-14 11:08:39 +01:00
elements.push(['function', {name: match[2], arguments: arguments, returns: match[5], returnsComment: returnsComment, comment: (c || [null])[0]}]);
2021-11-03 05:34:38 +01:00
}
code = functionFreeCode;
// looks for enums
let enumFreeCode = code;
2022-05-08 23:06:22 +02:00
const enums = /enum ([_\w]+) ?{([^}]*)}/g;
2021-11-03 05:34:38 +01:00
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], '');
2021-11-10 12:10:14 +01:00
const origIndex = originalCode.indexOf(match[0]);
const comment = /\/\*(.*?)\*\//gs;
let comments = [];
let comm;
while ((comm = comment.exec(originalCode.slice(0, origIndex))) !== null) {
comments.push([comm[1], comm.index + comm[0].length]);
}
comments = comments.filter(c => Math.abs(origIndex - c[1]) < 10);
const c = comments.pop();
//if (c) c[0] += 'isaac-lua-definitions debug: comment distance = ' + (origIndex - c[1]).toString();
if (options.v) log(undefined, ' enum ' + chalk.greenBright(match[1]));
2021-11-10 12:10:14 +01:00
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()}}), comment: (c || [null])[0]}]);
2021-11-03 05:34:38 +01:00
}
code = enumFreeCode;
// looks for constants / properties
let constFreeCode = code;
2022-05-08 23:06:22 +02:00
const consts = /([\w_]+)\s*:\s*([\w<>\s,]+)/g;
2021-11-03 05:34:38 +01:00
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], '');
2021-11-10 12:10:14 +01:00
const origIndex = originalCode.indexOf(match[0]);
const comment = /\/\*(.*?)\*\//gs;
let comments = [];
let comm;
while ((comm = comment.exec(originalCode.slice(0, origIndex))) !== null) {
comments.push([comm[1], comm.index + comm[0].length]);
}
comments = comments.filter(c => Math.abs(origIndex - c[1]) < 10);
const c = comments.pop();
//if (c) c[0] += 'isaac-lua-definitions debug: comment distance = ' + (origIndex - c[1]).toString();
elements.push(['const', {name: match[1], type: match[2], comment: (c || [null])[0]}]);
2021-11-03 05:34:38 +01:00
}
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;
}
async function recursiveReaddir(rpath, p) {
2021-11-03 09:58:21 +01:00
p = p || '';
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()) {
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;
}
let loggedFile = {};
function warn(file, string) {
if (!options.warn) return;
if (options.Sss) return;
if (!loggedFile[file]) {
console.log(` ${chalk.cyanBright(file)}`);
loggedFile[file] = true;
}
console.log(`${chalk.redBright('!')} ${string}`);
}
function log(file, string) {
if (options.s && string) return;
if (options.Ss) return;
if (options.Sss) return;
if (file && !loggedFile[file]) {
console.log(` ${chalk.cyanBright(file)}`);
loggedFile[file] = true;
}
if (string) console.log(' ' + string);
}
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 = {};
const files = (await recursiveReaddir(declarationsPath)).filter(f => {
return !f.includes('unofficial/') && (options.exclude.length == 0 || !options.exclude.includes(f)) && (options.only.length === 0 || options.only.includes(f))
});
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);
if (!options.Ss) log(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)) {
log('file exists in override/, ignoring');
2021-11-04 06:35:56 +01:00
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);
log(f, `parsed ${chalk.magentaBright(parsedElements)} objects from ${chalk.cyanBright(f)}`);
2021-11-03 09:58:21 +01:00
timeParsing += Date.now() - startParse;
2021-11-03 05:34:38 +01:00
2021-11-03 09:58:21 +01:00
if (parsed.length === 0) {
warn(f, `no elements parsed, ${chalk.gray('not writing anything')}`);
2021-11-03 09:58:21 +01:00
continue;
}
if (parsed.length > 10) {
warn(f, `over 10 top-level objects were parsed - this may be a bad idea`);
2021-11-03 09:58:21 +01:00
}
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;
}
log(f, `transpiled w/ final length of ${chalk.magentaBright(transpiled.length + ' chars')}`);
2021-11-03 09:58:21 +01:00
timeTotal += Date.now() - startParse;
2021-11-03 05:34:38 +01:00
2021-11-03 09:58:21 +01:00
if (transpiled.length === 0) {
warn(f, `nothing transpiled, ${chalk.gray('not writing anything')}`);
2021-11-03 09:58:21 +01:00
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(`\nfinished 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)}`);
2021-11-19 19:30:29 +01:00
})();