Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autogenerate TS typedefs for Core #5429

Merged
merged 22 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions GDevelop.js/Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,23 @@ module.exports = function (grunt) {
},
},
},
// Copy the library to newIDE
generateTypes: {
// Generate typings from the Bindings.idl
generateFlowTypes: {
command: 'node scripts/generate-types.js',
options: {
execOptions: {
cwd: __dirname,
},
},
},
generateTSTypes: {
command: 'node scripts/generate-dts.mjs',
options: {
execOptions: {
cwd: __dirname,
},
},
},
},
clean: {
options: { force: true },
Expand Down Expand Up @@ -148,6 +156,7 @@ module.exports = function (grunt) {
grunt.registerTask('build', [
'build:raw',
'shell:copyToNewIDE',
'shell:generateTypes',
'shell:generateFlowTypes',
'shell:generateTSTypes',
]);
};
6 changes: 6 additions & 0 deletions GDevelop.js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions GDevelop.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"license": "MIT",
"devDependencies": {
"@types/node": "^20.3.1",
"extend": "^2.0.1",
"grunt": "^1.0.1",
"grunt-contrib-clean": "^1.0.0",
Expand Down
214 changes: 214 additions & 0 deletions GDevelop.js/scripts/generate-dts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// @ts-check
import { readFileSync, writeFileSync } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));

const bindingsFile = readFileSync(
__dirname + '/../Bindings/Bindings.idl',
'utf-8'
);

const PrimitiveTypes = new Map([
['DOMString', 'string'],
['long', 'number'],
['double', 'number'],
['void', 'void'],
['boolean', 'boolean'],
]);

const none = Symbol('No default value');
class Parser {
static i = 0;
/** @private */
static str = '';
/** @private */
static l = 0;
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved

/** @param {string} str */
static set target(str) {
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
this.i = 0;
this.str = str;
this.l = str.length;
}
static get done() {
return this.i >= this.l;
}
static get c() {
return this.str.charAt(this.i);
}
static get cc() {
return this.str.charCodeAt(this.i);
}
static skipWhitespaces() {
while (this.cc <= 32) this.i++;
// Comments go everywhere whitespaces go and must be skipped as if it were whitespace
if (this.c === '/') {
this.i++;
if (this.c === '/') {
// //-style comments
this.skipUntil('\n');
} else if (this.c === '*') {
// /* */-style comments
do {
this.skipUntil('*');
} while (this.c !== '/');
this.i++;
} else console.warn(`Unexpected slash!`);
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
this.skipWhitespaces();
}
}
/** @param {string} thisCharacter */
static skipUntil(thisCharacter, skipOverIt = true) {
while (this.c !== thisCharacter && !this.done) this.i++;
if (skipOverIt) this.i++;
}
/** @param {string} thisCharacter */
static readUntil(thisCharacter, skipOverIt = true) {
let token = '';
while (this.c !== thisCharacter) {
token += this.c;
this.i++;
}
if (skipOverIt) this.i++;
return token;
}

static readType() {
// Ignore [Const] and such annotations
Parser.skipWhitespaces();
if (Parser.c === '[') Parser.skipUntil(']');
Parser.skipWhitespaces();

// Read the type
let type = Parser.readUntil(' ');
let optional = false;
if (type === 'optional') optional = true;
while (type === 'unsigned' || type === 'optional') {
// Re-read the type since unsigned is an unnecessary prefix for typescript
Parser.skipWhitespaces();
type = Parser.readUntil(' ');
}
Parser.skipWhitespaces();

return { type, optional };
}

static readIdentifier() {
let name = '';
while (
(Parser.cc >= 97 && Parser.cc <= 122) || // [a-z]
(Parser.cc >= 65 && Parser.cc <= 90) || // [A-Z]
(Parser.cc >= 48 && Parser.cc <= 57) || // [1-9]
Parser.c === '_'
) {
name += Parser.c;
Parser.i++;
}
return name;
}

static readNumber() {
let number = '';
while (
Parser.cc >= 48 &&
Parser.cc <= 57 // [1-9]
) {
number += Parser.c;
Parser.i++;
}
return parseInt(number, 10);
}
}

const interfaces = [];
for (const [a, interfaceName, interfaceCode] of bindingsFile.matchAll(
/interface ([a-zA-Z]+) {\r?\n?([^}]*)\r?\n}/gm
)) {
const methods = [];

Parser.target = interfaceCode;
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
while (!Parser.done) {
const { type: returnType, optional: optionalReturn } = Parser.readType();
const methodName = Parser.readUntil('(');
const isConstructor = returnType === 'void' && methodName === interfaceName;

/** @type {Array<{name:string, type:string, optional:boolean, defaultValue:(number | typeof none)}>} */
const parameters = [];
Parser.skipWhitespaces();
if (Parser.c !== ')')
do {
if (Parser.c === ',') Parser.i++;
const { type, optional } = Parser.readType();
Parser.skipWhitespaces();
const name = Parser.readIdentifier();
Parser.skipWhitespaces();

let defaultValue = none;
if (Parser.c === '=') {
Parser.i++;
Parser.skipWhitespaces();
defaultValue = Parser.readNumber();
Parser.skipWhitespaces();
}

parameters.push({ name, type, optional, defaultValue });
} while (Parser.c === ',');

// Health checks
if (!(Parser.c === ')')) console.warn('Expected closing paranthesis!');
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
Parser.i++;
Parser.skipWhitespaces();
if (!(Parser.c === ';')) console.warn('Expected semicolon!');
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
Parser.i++;
Parser.skipWhitespaces();

methods.push(
`${isConstructor ? `constructor` : methodName}(${parameters
.map(
({ name, type, optional, defaultValue }) =>
`${name}${optional ? '?' : ''}: ${
PrimitiveTypes.has(type) ? PrimitiveTypes.get(type) : type
}${defaultValue !== none ? ` = ${defaultValue}` : ''}`
)
.join(', ')}): ${
PrimitiveTypes.has(returnType)
? PrimitiveTypes.get(returnType)
: returnType
};`
);
}

interfaces.push(
`export class ${interfaceName} extends EmObject {
${methods.join('\n ')}
}`
);
}

const dts = `// Automatically generated by GDevelop.js/scripts/generate-dts.js

class EmObject {
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
/** The object's index in the WASM memory, and thus its unique identifier. */
ptr: number;

/**
* Call this to free the object's underlying memory. It may not be used afterwards.
*
* **Call with care** - if the object owns some other objects, those will also be destroyed,
* or if this object is owned by another object that does not expect it to be externally deleted
* (e.g. it is a child of a map), objects will be put in an invalid state that will most likely
* crash the app.
*
* WIth that said, be careful to do call this method when adequate, as otherwise the memory will
arthuro555 marked this conversation as resolved.
Show resolved Hide resolved
* never be freed, causing a memory leak, which is to be avoided.
*/
destroy(): void;
}

${interfaces.join('\n\n')}

export as namespace gd;
`;

writeFileSync(__dirname + '/../types.d.ts', dts);
Loading