Espruino/scripts/build_types.js
2023-09-27 14:57:47 +01:00

521 lines
16 KiB
JavaScript

#!/usr/bin/env node
/**
* Add two spaces at the beginning of every line.
* @param {string} string - The string to indent.
* @returns {string} The indented string.
*/
function indent(string) {
return string
.split("\n")
.map((line) => (line ? " " + line : line))
.join("\n");
}
/**
* Return the parameter's description.
* @param {string | string[]} - The description from the JSON comment.
* @returns {string} The description.
*/
function getParameterDescription(description) {
return !description
? ""
: typeof description === "string"
? description
: description.join("\n");
}
/**
* Return the documentation of the function or variable.
* @param {object} object - The object holding the function or variable.
* @returns {string} The object's documentation.
*/
function getDocumentation(object) {
// See https://jsdoc.app/ for how JSDoc comments are formatted
if (!object) return "";
return (
"/**\n" +
object
.getDescription()
.split("\n")
.filter((line) => line)
.map((line) => line)
.concat(object.type === "constructor" ? ["@constructor"] : [])
.concat(
object.type === "event"
? [
"@param {string} event - The event to listen to.",
`@param {${getArguments(
object
)} => void} callback - A function that is executed when the event occurs.${
object.params ? " Its arguments are:" : ""
}`,
].concat(
object.params
? object.params.map(([name, _, description]) =>
`* \`${name}\` ${getParameterDescription(
description
)}`.split("\n")
)
: []
)
: object.params
? [""].concat(
object.params
.map(([name, type, description]) => {
if (name === "this") name = "thisArg";
const desc = getParameterDescription(description);
return (
"@param {" +
getBasicType(type) +
"} " +
(desc.startsWith("[optional]") ? "[" + name + "]" : name) +
(!description
? ""
: typeof description === "string"
? " - " + desc
: "\n" + desc)
).split("\n");
})
.flat(1)
)
: []
)
.concat(
object.return
? [
`@returns {${getBasicType(object.return[0])}} ${
object.return[1] || ""
}`,
]
: []
)
.concat([`@url ${object.getURL()}`])
.map((line) => (" * " + line).trimEnd())
.join("\n") +
"\n */"
);
}
/**
* Convert a basic type to its corresponding TypeScript type.
* A "basic type" is any but an object or a function.
* @param {string} type - The basic type.
* @returns {string} The TypeScript type.
*/
function getBasicType(type) {
if (!type) return "any";
if (["int", "float", "int32"].includes(type)) return "number";
if (type == "pin") return "Pin";
if (type == "String") return "string";
if (type == "bool") return "boolean";
if (type == "JsVarArray") return "any";
if (type == "JsVar") return "any";
if (type == "Array") return "any[]";
if (type == "Promise") return "Promise<void>";
return type;
}
/**
* Return the arguments of the method in TypeScript format.
* @param {object} method - The object containing the method's data.
* @returns {string} The argument list including brackets.
*/
function getArguments(method) {
let args = [];
if ("params" in method)
args = method.params.map((param) => {
// hack because digitalRead/Write can also take arrays/objects (but most use cases are Pins)
if (param[0] == "pin" && param[1] == "JsLet") param[1] = "Pin";
if (param[0] === "function") param[0] = "func";
if (param[0] === "var") param[0] = "variable";
if (param[0] === "this") param[0] = "thisArg";
let doc = typeof param[2] === "string" ? param[2] : param[2].join("\n");
let optional = doc && doc.startsWith("[optional]");
let rest = param[1] === "JsVarArray";
return (
(rest ? "..." : "") +
param[0] +
(optional ? "?" : "") +
": " +
getBasicType(param[1]) +
(rest ? "[]" : "")
);
});
return "(" + args.join(", ") + ")";
}
/**
* Return the return type of the method in TypeScript format.
* @param {object} method - The object containing the method's data.
* @returns {string} The return type.
*/
function getReturnType(method) {
if ("return_object" in method) return getBasicType(method.return_object);
if ("return" in method) return getBasicType(method.return[0]);
return "void";
}
/**
* Return the declaration of a function or variable.
* @param {object} object - The object containing the function or variable's data.
* @param {string} [context] - Either "global", "class" or "module".
* @returns {string} The function or variable's declaration.
*/
function getDeclaration(object, context) {
if ("typescript" in object) {
let declaration =
typeof object.typescript === "string"
? object.typescript
: object.typescript.join("\n");
if (!declaration.startsWith("function") && context === "library") {
declaration = "function " + declaration;
}
return declaration;
}
if (object.type === "event") {
if (context === "class") {
return `on(event: "${object.name}", callback: ${getArguments(
object
)} => void): void;`;
} else {
return `function on(event: "${object.name}", callback: ${getArguments(
object
)} => void): void;`;
}
} else if (
["function", "method", "staticmethod", "constructor"].includes(object.type)
) {
// function
const name = object.type === "constructor" ? "new" : object.name;
return `${context === "global" ? "declare " : ""}${
context !== "class" ? "function " : ""
}${name}${getArguments(object)}: ${getReturnType(object)};`;
} else {
// property
const type =
object.type === "object"
? object.instanceof
: getBasicType(object.return_object || object.return[0]);
return `${context === "global" ? "declare " : ""}${
context !== "class" ? "const " : ""
}${object.name}: ${type};`;
}
}
/**
* Return classes and libraries.
* @param {object[]} objects - The list of objects.
* @returns {object}
* An object with class names as keys and the following as values:
* {
* library?: true, // whether it's a library or a class
* object?, // the object containing its data
* staticProperties: [], // a list of its static properties
* prototype: [], // a list of the prototype's properties
* cons?: // the class's constructor
* }
*/
function getClasses(objects) {
const classes = {};
objects.forEach(function (object) {
if (object.typescript === null) return;
if (object.type == "class" || object.type == "library") {
classes[object.class] = {
library: object.type === "library",
object,
staticProperties: [],
prototype: [],
};
}
});
return classes;
}
/**
* Return all the objects in an organised structure, so class are
* found inside their corresponding classes.
* @param {object[]} objects - The list of objects.
* @returns {[object, object[]]}
* An array. The first item is the classes object (see `getClasses`),
* and the second is an array of global objects.
*/
function getAll(objects) {
const classes = getClasses(objects);
const globals = [];
/**
* @param {string} c - The name of the class.
* @returns {object}
* The class with the corresponding name (see `getClasses` for its
* contents), or a new one if it doesn't exist.
*/
function getClass(c) {
if (!classes[c]) classes[c] = { staticProperties: [], prototype: [] };
return classes[c];
}
objects.forEach(function (object) {
if (object.typescript === null) return;
if (["class", "library"].includes(object.type)) {
// already handled in `getClases`
} else if (
["include", "init", "idle", "kill", "hwinit", "EV_SERIAL1"].includes(
object.type
)
) {
// internal
} else if (object.type === "constructor") {
// set as constructor
getClass(object.class).cons = object;
} else if (
["event", "staticproperty", "staticmethod"].includes(object.type)
) {
// add to static properties
getClass(object.class).staticProperties.push(object);
} else if (["property", "method"].includes(object.type)) {
// add to prototype
getClass(object.class)["prototype"].push(object);
} else if (
["function", "letiable", "object", "variable", "typescript"].includes(
object.type
)
) {
// add to globals
globals.push(object);
} else console.warn("Unknown type " + object.type + " for ", object);
});
return [classes, globals];
}
/**
* Return the declarations of custom types.
* @param {object[]} types - The list of types defined in comments.
* Of the form { declaration: string, implementation: string }.
* @returns {string} The joined declarations.
*/
function getTypeDeclarations(types) {
return (
"// TYPES\n" +
types
.filter((type) => !type.class)
.map((type) =>
type.declaration.replace(/\\\//g, "/").replace(/\\\\/g, "\\")
)
.join("")
);
}
/**
* Get the declaration of a builtin class, that is, that exists in
* vanilla JavaScript, e.g. String, Array.
* @param {string} name - The class's name.
* @param {object} c - The class's data.
* @param {object[]} types
* @returns {string} The class's declaration.
*/
function getBuiltinClassDeclaration(name, c, types) {
return (
`interface ${name}Constructor {\n` +
indent(
c.staticProperties
.concat([c.cons])
.filter((property) => property)
.map((property) =>
`${getDocumentation(property)}\n${getDeclaration(
property,
"class"
)}`.trim()
)
.join("\n\n")
) +
`\n}\n\n` +
(name.endsWith("Array") && !name.startsWith("Array") // is a typed array?
? `type ${name} = ArrayBufferView<${name}>;\n`
: `interface ${c.object?.typescript || name} {\n` +
indent(
c.prototype
.map((property) =>
`${getDocumentation(property)}\n${getDeclaration(
property,
"class"
)}`.trim()
)
.concat(name === "Array" ? ["[index: number]: T"] : [])
.concat(types.map((type) => type.declaration))
.join("\n\n")
) +
`\n}\n\n${getDocumentation(c.object)}`) +
`\ndeclare const ${name}: ${name}Constructor`
);
}
/**
* Get the declaration of a class that is not builtin.
* @param {string} name - The class's name.
* @param {object} c - The class's data.
* @param {object[]} types
* @returns {string} The class's declaration.
*/
function getOtherClassDeclaration(name, c, types) {
return (
`${getDocumentation(c.object)}\ndeclare class ${
c.object?.typescript || name
} {\n` +
indent(
c.staticProperties
.concat([c.cons])
.filter((property) => property)
.map((property) =>
`${getDocumentation(property)}\n${getDeclaration(property, "class")
.split("\n")
.map((dec) => "static " + dec)
.join("\n")}`.trim()
)
.join("\n\n") +
"\n\n" +
c.prototype
.map((property) =>
`${getDocumentation(property)}\n${getDeclaration(
property,
"class"
)}`.trim()
)
.concat(name === "ArrayBufferView" ? ["[index: number]: number"] : [])
.concat(types.map((type) => type.declaration))
.join("\n\n")
) +
"\n}"
);
}
/**
* Return the class declarations (not including libraries).
* @param {object} classes - The object of classes (see `getClasses`).
* @param {object[]} types
* @returns {string} The class declarations.
*/
function getClassDeclarations(classes, types) {
return (
"\n\n// CLASSES\n\n" +
Object.entries(classes)
.filter(([_, c]) => !c.library)
.map(([name, c]) =>
name in global
? getBuiltinClassDeclaration(
name,
c,
types.filter((type) => type.class === name)
)
: getOtherClassDeclaration(
name,
c,
types.filter((type) => type.class === name)
)
)
.join("\n\n")
);
}
/**
* Return the global declarations.
* @param {object[]} globals - The list of global objects.
* @returns {string} The global declarations.
*/
function getGlobalDeclarations(globals, classes) {
return (
"\n\n// GLOBALS\n\n" +
globals
.map((global) =>
global.name === "require"
? Object.entries(classes)
.filter(([_, c]) => c.library)
.map(
([name]) =>
`declare function require(moduleName: "${name}"): typeof import("${name}");`
)
.concat(["declare function require(moduleName: string): any;"])
.join("\n")
: global.name === "global"
? `declare const global: {\n` +
indent(
globals
.map((global) => `${global.name}: typeof ${global.name};`)
.concat("[key: string]: any;")
.join("\n")
) +
"\n}"
: `${getDocumentation(global)}\n${getDeclaration(global, "global")}`
)
.join("\n\n")
);
}
/**
* Return the library declarations.
* @param {object} classes - The object of classes and libraries (see `getClasses`).
* @returns {string} The library declarations.
*/
function getLibraryDeclarations(classes) {
return (
"\n\n// LIBRARIES\n\n" +
Object.entries(classes)
.filter(([_, c]) => c.library)
.map(
([name, library]) =>
`${getDocumentation(library.object)}\ndeclare module "${name}" {\n` +
indent(
library.staticProperties
.map((property) =>
`${getDocumentation(property)}\n${getDeclaration(
property,
"library"
)}`.trim()
)
.join("\n\n")
) +
"\n}"
)
.join("\n\n")
);
}
/**
* Build TypeScript declarations from the source code's comments.
* @returns {Promise<string>} Promise that is resolved with the contents of the file to write.
*/
function buildTypes() {
return new Promise((resolve) => {
require("./common.js").readAllWrapperFiles(function (objects, types) {
const [classes, globals] = getAll(objects, types);
resolve(
"// Type definitions for Espruino latest\n" +
"// Project: http://www.espruino.com/, https://github.com/espruino/espruinotools" +
"// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped\n\n" +
'/// <reference path="other.d.ts" />\n\n' +
getTypeDeclarations(types) +
getClassDeclarations(classes, types) +
getGlobalDeclarations(globals, classes) +
getLibraryDeclarations(classes)
);
});
});
}
buildTypes().then((content) => {
require("fs").writeFileSync(
__dirname + "/../../BangleApps/typescript/types/main.d.ts",
content
);
// Write to DefinitelyTyped if repository exists
try {
require("fs").writeFileSync(
__dirname + "/../../DefinitelyTyped/types/espruino/index.d.ts",
content
);
} catch (e) {}
console.log("Generated build types!");
});