mirror of
https://github.com/fleetbase/fleetbase.git
synced 2025-12-19 14:18:57 +00:00
368 lines
15 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
},
|
|
};
|