Files
Fleetbase-Mirror-Repo/console/lib/fleetbase-extensions-generator/index.js
2025-11-28 11:56:38 +08:00

368 lines
15 KiB
JavaScript

/* eslint-env node */
'use strict';
const fg = require('fast-glob');
const fs = require('fs');
const path = require('path');
const recast = require('recast');
const babelParser = require('recast/parsers/babel');
const builders = recast.types.builders;
const chokidar = require('chokidar');
module.exports = {
name: require('./package').name,
getGeneratedFileHeader() {
const year = (new Date()).getFullYear();
return `/**
* ███████╗██╗ ███████╗███████╗████████╗██████╗ █████╗ ███████╗███████╗
* ██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝
* █████╗ ██║ █████╗ █████╗ ██║ ██████╔╝███████║███████╗█████╗
* ██╔══╝ ██║ ██╔══╝ ██╔══╝ ██║ ██╔══██╗██╔══██║╚════██║██╔══╝
* ██║ ███████╗███████╗███████╗ ██║ ██████╔╝██║ ██║███████║███████╗
* ╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝
*
* AUTO-GENERATED FILE - DO NOT EDIT
*
* This file is automatically generated by the Fleetbase extension build system.
* Any manual changes will be overwritten on the next build.
*
* @generated
* @copyright © ${year} Fleetbase Pte Ltd. All rights reserved.
* @license AGPL-3.0-or-later
*/
`;
},
included(app) {
this._super.included.apply(this, arguments);
console.log('\n' + '/'.repeat(70));
console.log('[Fleetbase] Extension Build System');
console.log('/'.repeat(70));
// Generate files on startup
this.generateExtensionFiles();
// Watch for changes in development
this.watchExtensionFiles();
},
async generateExtensionFiles() {
const extensions = await this.getExtensions();
if (extensions.length === 0) {
console.log('[Fleetbase] No extensions found');
return;
}
console.log(`[Fleetbase] Discovered ${extensions.length} extension(s)`);
extensions.forEach(ext => {
console.log(`[Fleetbase] - ${ext.name} (v${ext.version})`);
});
console.log('');
// Generate extension shims
this.generateExtensionShims(extensions);
// Generate extension loaders
this.generateExtensionLoaders(extensions);
// Generate router
this.generateRouter(extensions);
// Generate manifest
this.generateExtensionsManifest(extensions);
},
getExtensions() {
return new Promise((resolve, reject) => {
const extensions = [];
const seenPackages = new Set();
const cwd = this.project.root;
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'], { cwd })
.then((results) => {
for (let i = 0; i < results.length; i++) {
const packagePath = path.join(cwd, results[i]);
const packageJson = fs.readFileSync(packagePath);
let packageData = null;
try {
packageData = JSON.parse(packageJson);
} catch (e) {
console.warn(`Could not parse package.json at ${packagePath}:`, e);
continue;
}
if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) {
continue;
}
// If we've seen this package before, skip it
if (seenPackages.has(packageData.name)) {
continue;
}
seenPackages.add(packageData.name);
extensions.push(this.only(packageData, ['name', 'description', 'version', 'fleetbase', 'keywords', 'license', 'repository']));
}
resolve(extensions);
})
.catch(reject);
});
},
only(subject, props = []) {
const keys = Object.keys(subject);
const result = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (props.includes(key)) {
result[key] = subject[key];
}
}
return result;
},
getExtensionMountPath(extensionName) {
let extensionNameSegments = extensionName.split('/');
let mountName = extensionNameSegments[1];
if (typeof mountName !== 'string') {
mountName = extensionNameSegments[0];
}
return mountName.replace('-engine', '');
},
generateExtensionShims(extensions) {
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
if (!fs.existsSync(extensionsDir)) {
fs.mkdirSync(extensionsDir, { recursive: true });
}
extensions.forEach((extension) => {
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (!fs.existsSync(extensionPath)) {
return;
}
const extensionContent = fs.readFileSync(extensionPath, 'utf8');
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
const shimFile = path.join(extensionsDir, `${mountPath}.js`);
const fileContent = this.getGeneratedFileHeader() + extensionContent;
fs.writeFileSync(shimFile, fileContent, 'utf8');
console.log(`[Fleetbase] ✓ Generated app/extensions/${mountPath}.js`);
});
},
generateExtensionLoaders(extensions) {
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
if (!fs.existsSync(extensionsDir)) {
fs.mkdirSync(extensionsDir, { recursive: true });
}
const imports = [];
const loaders = {};
extensions.forEach((extension) => {
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (!fs.existsSync(extensionPath)) {
return;
}
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
const camelCaseName = mountPath.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
imports.push(`import ${camelCaseName} from './${mountPath}';`);
loaders[extension.name] = `() => ${camelCaseName}`;
});
const loadersContent = this.getGeneratedFileHeader() + `${imports.join('\n')}
export const EXTENSION_LOADERS = {
${Object.entries(loaders)
.map(([name, loader]) => ` '${name}': ${loader}`)
.join(',\n')}
};
export const getExtensionLoader = (packageName) => {
return EXTENSION_LOADERS[packageName];
};
export default getExtensionLoader;
`;
const loadersFile = path.join(extensionsDir, 'index.js');
fs.writeFileSync(loadersFile, loadersContent, 'utf8');
console.log(`[Fleetbase] ✓ Generated app/extensions/index.js`);
},
generateRouter(extensions) {
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
const routerMapPath = path.join(this.project.root, 'router.map.js');
const routerFileContents = fs.readFileSync(routerMapPath, 'utf-8');
const ast = recast.parse(routerFileContents, { parser: babelParser });
recast.visit(ast, {
visitCallExpression(path) {
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
let functionExpression;
// Find the function expression
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
consoleExtensions.forEach((extension) => {
const mountPath = module.exports.getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
// Check if engine is already mounted
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
// If not mounted, append to the function body
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
let functionExpression;
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
rootExtensions.forEach((extension) => {
const mountPath = module.exports.getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
this.traverse(path);
},
});
const output = recast.print(ast, { quote: 'single' }).code;
const routerFile = path.join(this.project.root, 'app/router.js');
const fileContent = this.getGeneratedFileHeader() + output;
fs.writeFileSync(routerFile, fileContent);
console.log(`[Fleetbase] ✓ Generated app/router.js`);
},
generateExtensionsManifest(extensions) {
const publicDir = path.join(this.project.root, 'public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
const manifest = extensions.map((ext) => ({
name: ext.name,
version: ext.version,
route: ext.fleetbase?.route,
}));
const manifestFile = path.join(publicDir, 'extensions.json');
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8');
console.log(`[Fleetbase] ✓ Generated public/extensions.json`);
},
watchExtensionFiles() {
const isDevelopment = process.env.EMBER_ENV !== 'production';
if (!isDevelopment) {
return;
}
const self = this;
this.getExtensions().then((extensions) => {
const extensionFiles = [];
extensions.forEach((extension) => {
const extensionPath = path.join(self.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (fs.existsSync(extensionPath)) {
extensionFiles.push(extensionPath);
}
});
if (extensionFiles.length === 0) {
return;
}
const watcher = chokidar.watch(extensionFiles, {
persistent: true,
ignoreInitial: true,
});
watcher.on('change', (filePath) => {
console.log(`\n[Fleetbase] Extension file changed: ${path.basename(filePath)}`);
console.log('[Fleetbase] Regenerating extension files...\n');
self.generateExtensionFiles();
});
});
},
};