Files
GDevelop/newIDE/app/scripts/extract-extensions-document.js
2025-11-06 17:33:11 +01:00

508 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-check
/**
* Launch this script to generate a list (in markdown format) of all custom extensions.
*/
const initializeGDevelopJs = require('../public/libGD.js');
const fs = require('fs').promises;
const { default: axios } = require('axios');
const path = require('path');
const {
gdevelopWikiUrlRoot,
getHelpLink,
getExtensionFolderName,
improperlyFormattedHelpPaths,
generateSvgImageIcon,
} = require('./lib/WikiHelpLink');
const {
convertCommonMarkdownToPythonMarkdown,
} = require('./lib/PythonMarkdownHelper');
const shell = require('shelljs');
const {
generateExtensionReference,
generateExtensionRawText,
rawTextsToString,
} = require('./lib/ExtensionReferenceGenerator');
const { mapVector, mapFor } = require('./lib/MapFor');
const { groupBy, sortKeys } = require('./lib/ArrayHelpers');
const { generateAllExtensionsSections } = require('./lib/WikiExtensionTable');
/** @typedef {import("./lib/ExtensionReferenceGenerator.js").RawText} RawText */
/** @typedef {import('../../../GDevelop.js/types').EventsFunctionsExtension} EventsFunctionsExtension */
/** @typedef {import('../../../GDevelop.js/types').Project} Project */
/** @typedef {import('../../../GDevelop.js/types').PlatformExtension} PlatformExtension */
/** @typedef {{ tier: 'community' | 'experimental' | 'reviewed', shortDescription: string, authorIds: Array<string>, authors?: Array<{id: string, username: string}>, extensionNamespace: string, fullName: string, name: string, version: string, gdevelopVersion?: string, url: string, headerUrl: string, tags: Array<string>, category: string, previewIconUrl: string, eventsBasedBehaviorsCount: number, eventsFunctionsCount: number}} ExtensionShortHeader */
const extensionShortHeadersUrl =
'https://api.gdevelop-app.com/asset/extension-short-header';
const gdRootPath = path.join(__dirname, '..', '..', '..');
const outputRootPath = path.join(gdRootPath, 'docs-wiki');
const extensionsRootPath = path.join(outputRootPath, 'extensions');
const extensionsMainFilePath = path.join(extensionsRootPath, 'index.md');
/**
* @param {{id: string, username: string}[]} authors
*/
const generateAuthorNamesWithLinks = authors => {
const authorAndLinks = authors
.map(author => {
if (!author.username) return null;
return `[${author.username}](https://gd.games/${author.username})`;
})
.filter(Boolean)
.join(', ');
return authorAndLinks ? authorAndLinks : '(not specified)';
};
/**
* Add a serialized (JS object) events function extension to the project.
*
* (useful as containing author public profiles information).
*
* @param {any} gd
* @param {Project} project
*
* @returns {Promise<Array<EventsFunctionsExtension>>}
*/
const addAllExtensionsToProject = async (gd, project) => {
const response = await axios.get(extensionShortHeadersUrl);
const extensionShortHeaders = response.data;
if (!extensionShortHeaders.length) {
throw new Error('Unexpected response from the extension endpoint.');
}
return await Promise.all(
extensionShortHeaders.map(async extensionShortHeader => {
const response = await axios.get(extensionShortHeader.url);
const serializedExtension = response.data;
if (!serializedExtension) {
throw new Error(
`Unexpected response when fetching an extension (${
extensionShortHeader.url
}).`
);
}
const { name } = serializedExtension;
if (!name)
return Promise.reject(new Error('Malformed extension (missing name).'));
const newEventsFunctionsExtension = project.insertNewEventsFunctionsExtension(
name,
0
);
unserializeFromJSObject(
gd,
newEventsFunctionsExtension,
serializedExtension,
'unserializeFrom',
project
);
return newEventsFunctionsExtension;
})
);
};
/**
* Tool function to restore a serializable object from a JS object.
* Most gd.* objects are "serializable", meaning they have a serializeTo
* and unserializeFrom method.
* @param {any} serializable A gd.* object to restore (gdSerializable)
* @param {Object} object The JS object to be used to restore the serializable.
* @param {string} methodName The name of the unserialization method. "unserializeFrom" by default
* @param {?any} optionalProject The project to pass as argument for unserialization (gdProject)
*/
function unserializeFromJSObject(
gd,
serializable,
object,
methodName = 'unserializeFrom',
optionalProject = undefined
) {
const serializedElement = gd.Serializer.fromJSObject(object);
if (!optionalProject) {
serializable[methodName](serializedElement);
} else {
// It's not uncommon for unserializeFrom methods of gd.* classes
// to require the project to be passed as first argument.
serializable[methodName](optionalProject, serializedElement);
}
serializedElement.delete();
}
/**
* Return the list of all extensions and their associated short headers
* (useful as containing author public profiles information).
* @returns {Promise<Array<ExtensionShortHeader>>} A promise to all extension headers
*/
const getAllExtensionShortHeaders = async () => {
const response = await axios.get(extensionShortHeadersUrl);
const extensionShortHeaders = response.data;
if (!extensionShortHeaders.length) {
throw new Error('Unexpected response from the extension endpoint.');
}
return extensionShortHeaders;
};
/**
* Create a page for an extension.
* @param {any} gd
* @param {any} project (gdProject)
* @param {EventsFunctionsExtension} eventsFunctionsExtension
* @param {ExtensionShortHeader} extensionShortHeader
* @param {boolean} isExperimental The tier
*/
const createExtensionReferencePage = async (
gd,
project,
eventsFunctionsExtension,
extensionShortHeader,
isExperimental
) => {
const platformExtension = generateEventsFunctionExtensionMetadata(
gd,
project,
eventsFunctionsExtension
);
const extensionReference = generateExtensionReference({
platform: gd.JsPlatform.get(),
extension: platformExtension,
eventsFunctionsExtension,
});
const referencePageContent = rawTextsToString(
generateExtensionRawText(
extensionReference,
reference =>
generateExtensionHeaderText(
reference,
extensionShortHeader,
isExperimental
),
generateExtensionFooterText
)
);
const folderName = getExtensionFolderName(platformExtension.getName());
const extensionReferenceFilePath = path.join(
extensionsRootPath,
folderName,
'index.md'
);
await fs.mkdir(path.dirname(extensionReferenceFilePath), {
recursive: true,
});
await fs.writeFile(extensionReferenceFilePath, referencePageContent);
console.info(`️ File generated: ${extensionReferenceFilePath}`);
};
/**
*
* @param {{ extension: any }} extension (gdPlatformExtension)
* @param {ExtensionShortHeader} extensionShortHeader
* @param {boolean} isExperimental
* @returns {RawText}
*/
const generateExtensionHeaderText = (
{ extension },
extensionShortHeader,
isExperimental
) => {
const folderName = getExtensionFolderName(extension.getName());
const referencePageUrl = `${gdevelopWikiUrlRoot}/extensions/${folderName}`;
const helpPageUrl = getHelpLink(extension.getHelpPath()) || referencePageUrl;
const authorNamesWithLinks = generateAuthorNamesWithLinks(
extensionShortHeader.authors || []
);
return {
text:
`# ${extension.getFullName()}` +
'\n\n' +
generateSvgImageIcon(extensionShortHeader.previewIconUrl) +
'\n' +
`${extensionShortHeader.shortDescription}\n` +
'\n' +
`**Authors and contributors** to this experimental extension: ${authorNamesWithLinks}.\n` +
'\n' +
(isExperimental
? `!!! warning
This is an extension made by a community member and it only got through a
light review by the GDevelop extension team. As such, we can't guarantee it
meets all the quality standards of fully reviewed extensions.\n\n`
: '') +
'---\n' +
'\n' +
convertCommonMarkdownToPythonMarkdown(extension.getDescription()) +
'\n' +
(extension.getHelpPath() ? `\n[Read more...](${helpPageUrl})\n` : ``) +
'\n' +
`!!! tip
Learn [how to install new extensions](/gdevelop5/extensions/search) by following a step-by-step guide.` +
'\n',
};
};
/** @returns {RawText} */
const generateExtensionFooterText = ({ extension }) => {
return {
text:
`
---
*This page is an auto-generated reference page about the **${extension.getFullName()}** extension for [GDevelop, the open-source, AI-powered, cross-platform game engine designed for everyone](https://gdevelop.io/).*` +
' ' +
'Learn more about [all GDevelop extensions here](/gdevelop5/extensions).',
};
};
/**
* Generate the metadata for the events based extension
* @param {Project} project
* @param {EventsFunctionsExtension} eventsFunctionsExtension
* @returns {PlatformExtension}
*/
const generateEventsFunctionExtensionMetadata = (
gd,
project,
eventsFunctionsExtension
) => {
const extension = new gd.PlatformExtension();
gd.MetadataDeclarationHelper.declareExtension(
extension,
eventsFunctionsExtension
);
// Generate all behaviors and their functions
mapVector(
eventsFunctionsExtension.getEventsBasedBehaviors(),
eventsBasedBehavior => {
const behaviorMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateBehaviorMetadata(
project,
extension,
eventsFunctionsExtension,
eventsBasedBehavior,
behaviorMethodMangledNames
);
behaviorMethodMangledNames.delete();
}
);
// Generate all objects and their functions
mapVector(
eventsFunctionsExtension.getEventsBasedObjects(),
eventsBasedObject => {
const objectMethodMangledNames = new gd.MapStringString();
gd.MetadataDeclarationHelper.generateObjectMetadata(
project,
extension,
eventsFunctionsExtension,
eventsBasedObject,
objectMethodMangledNames
);
objectMethodMangledNames.delete();
}
);
// Generate all free functions
const metadataDeclarationHelper = new gd.MetadataDeclarationHelper();
const freeEventsFunctions = eventsFunctionsExtension.getEventsFunctions();
mapFor(0, freeEventsFunctions.getEventsFunctionsCount(), i => {
const eventsFunction = freeEventsFunctions.getEventsFunctionAt(i);
metadataDeclarationHelper.generateFreeFunctionMetadata(
project,
extension,
eventsFunctionsExtension,
eventsFunction
);
});
metadataDeclarationHelper.delete();
return extension;
};
/**
* @param {Array<any>} extensions The extension (gdEventsFunctionsExtension)
*/
const generateExtensionsPageList = (extensions, indentationLevel) => {
const extensionsByCategory = sortKeys(
groupBy(extensions, pair => pair.getCategory() || 'General')
);
const baseIndentation = ' '.repeat(4 * indentationLevel);
let pagesList = '';
for (const category in extensionsByCategory) {
pagesList += `${baseIndentation}- ${category}:\n`;
const extensions = extensionsByCategory[category];
for (const extension of extensions) {
const folderName = getExtensionFolderName(extension.getName());
pagesList += `${baseIndentation} - ${extension.getFullName()}: ${folderName}\n`;
}
}
return pagesList.length === 0
? pagesList
: pagesList.substring(0, pagesList.length - 1);
};
/**
* @param {Array<any>} reviewedExtensions The extension (gdEventsFunctionsExtension)
* @param {Array<any>} experimentalExtensions The extension (gdEventsFunctionsExtension)
*/
const generateExtensionsMkDocsDotPagesFile = async (
reviewedExtensions,
experimentalExtensions
) => {
const dotPagesContent = `nav:
- index.md
- search.md
- tiers.md
${generateExtensionsPageList(reviewedExtensions, 1)}
- Experimental extensions:
${generateExtensionsPageList(experimentalExtensions, 2)}
- ...
- Create your own extensions:
- Create a new extension : create.md
- best-practices.md
- share-extension.md
`;
const extensionsDotPagesFilePath = path.join(extensionsRootPath, '.pages');
await fs.writeFile(extensionsDotPagesFilePath, dotPagesContent);
console.info(`️ File generated: ${extensionsDotPagesFilePath}`);
};
const generateExtensionsList = async gd => {
let content = '';
const project = new gd.ProjectHelper.createNewGDJSProject();
await addAllExtensionsToProject(gd, project);
const extensionShortHeaders = await getAllExtensionShortHeaders();
const reviewedExtensionShortHeaders = extensionShortHeaders.filter(
header => header.tier !== 'community' && header.tier !== 'experimental'
);
const experimentalExtensionShortHeaders = extensionShortHeaders.filter(
header => header.tier === 'community' || header.tier === 'experimental'
);
const reviewedExtensions = reviewedExtensionShortHeaders.map(header =>
project.getEventsFunctionsExtension(header.name)
);
const experimentalExtensions = experimentalExtensionShortHeaders.map(header =>
project.getEventsFunctionsExtension(header.name)
);
content += '## Reviewed extensions\n\n';
for (const extension of reviewedExtensions) {
const extensionShortHeader = extensionShortHeaders.find(
header => header.name === extension.getName()
);
if (!extensionShortHeader) {
throw new Error(
`Could not find header for extension: ${extension.getName()}`
);
}
await createExtensionReferencePage(
gd,
project,
extension,
extensionShortHeader,
false
);
}
content += generateAllExtensionsSections({
extensions: reviewedExtensions,
baseFolder: 'extensions',
});
content += `## Experimental extensions
The following extensions are made by a community members and they only got
though a light review by the GDevelop extension team. As such, we can't
guarantee they meet all the quality standards of fully reviewed extensions.
`;
for (const extension of experimentalExtensions) {
const extensionShortHeader = extensionShortHeaders.find(
header => header.name === extension.getName()
);
if (!extensionShortHeader) {
throw new Error(
`Could not find header for extension: ${extension.getName()}`
);
}
await createExtensionReferencePage(
gd,
project,
extension,
extensionShortHeader,
true
);
}
content += generateAllExtensionsSections({
extensions: experimentalExtensions,
baseFolder: 'extensions',
});
await generateExtensionsMkDocsDotPagesFile(
reviewedExtensions,
experimentalExtensions
);
project.delete();
return content;
};
initializeGDevelopJs().then(async gd => {
try {
console.info(`️ Loading all community extensions...`);
let indexPageContent = `---
icon: material/star-plus
---
# Extensions
In addition to [core features](/gdevelop5/all-features), new capabilities are provided by extensions. Extensions can contain objects, behaviors, actions, conditions, expressions, effects or events.
Official as well as experimental extensions are accessible [directly from GDevelop](/gdevelop5/extensions/search).
A list of [community-made extensions is available on GitHub](https://github.com/GDevelopApp/GDevelop-community-list).
New extensions can also be [created](/gdevelop5/extensions/create) from scratch using events or JavaScript.
Read more about this:
* [Create your own extensions](/gdevelop5/extensions/create)
* [Share extensions with the community](/gdevelop5/extensions/share-extension)
* [Use JavaScript in events](/gdevelop5/events/js-code)
`;
indexPageContent += await generateExtensionsList(gd);
try {
await fs.mkdir(path.dirname(extensionsMainFilePath), { recursive: true });
await fs.writeFile(extensionsMainFilePath, indexPageContent);
console.info(`✅ Done. File generated: ${extensionsMainFilePath}`);
} catch (err) {
console.error('❌ Error while writing output', err);
shell.exit(1);
}
if (improperlyFormattedHelpPaths.size > 0) {
console.info(
`⚠️ Extensions documents generated, but some help paths are invalid:`,
improperlyFormattedHelpPaths.keys()
);
} else {
console.info(`✅ Extensions documents generated.`);
}
} catch (err) {
console.error('❌ Error while fetching data', err);
shell.exit(1);
}
});