mirror of
https://github.com/espruino/Espruino.git
synced 2025-12-08 19:06:15 +00:00
521 lines
16 KiB
JavaScript
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!");
|
|
});
|