From b27e485a44f44a61135b2cc57b495bc018b4d7bc Mon Sep 17 00:00:00 2001 From: roncodes <816371+roncodes@users.noreply.github.com> Date: Thu, 27 Nov 2025 05:23:38 -0500 Subject: [PATCH] fix: Router mount context issue and refactor into modular utilities - Fixed 'this' context issue in addConsoleExtensions/addRootExtensions - Refactored monolithic index.js into separate utility modules: - discover-extensions.js - Extension discovery logic - generate-extension-shims.js - Shim file generation - generate-extension-loaders.js - Loader map generation - generate-router.js - Router AST manipulation - generate-manifest.js - Manifest generation - watch-extensions.js - File watching logic - Simplified index.js to orchestrate utilities - Improved code organization and maintainability --- .../fleetbase-extensions-generator/index.js | 490 ++---------------- .../utils/discover-extensions.js | 39 ++ .../utils/generate-extension-loaders.js | 62 +++ .../utils/generate-extension-shims.js | 61 +++ .../utils/generate-manifest.js | 34 ++ .../utils/generate-router.js | 209 ++++++++ .../utils/watch-extensions.js | 42 ++ 7 files changed, 480 insertions(+), 457 deletions(-) create mode 100644 console/lib/fleetbase-extensions-generator/utils/discover-extensions.js create mode 100644 console/lib/fleetbase-extensions-generator/utils/generate-extension-loaders.js create mode 100644 console/lib/fleetbase-extensions-generator/utils/generate-extension-shims.js create mode 100644 console/lib/fleetbase-extensions-generator/utils/generate-manifest.js create mode 100644 console/lib/fleetbase-extensions-generator/utils/generate-router.js create mode 100644 console/lib/fleetbase-extensions-generator/utils/watch-extensions.js diff --git a/console/lib/fleetbase-extensions-generator/index.js b/console/lib/fleetbase-extensions-generator/index.js index a7cdcbe4..fab77f4f 100644 --- a/console/lib/fleetbase-extensions-generator/index.js +++ b/console/lib/fleetbase-extensions-generator/index.js @@ -1,479 +1,55 @@ +/* eslint-env node */ 'use strict'; -const path = require('path'); -const fs = require('fs'); -const fg = require('fast-glob'); - -console.log('[fleetbase-extensions-generator] Addon loaded at startup'); +const discoverExtensions = require('./utils/discover-extensions'); +const generateExtensionShims = require('./utils/generate-extension-shims'); +const generateExtensionLoaders = require('./utils/generate-extension-loaders'); +const generateRouter = require('./utils/generate-router'); +const generateExtensionsManifest = require('./utils/generate-manifest'); +const watchExtensions = require('./utils/watch-extensions'); module.exports = { - name: 'fleetbase-extensions-generator', - - isDevelopingAddon() { - return true; - }, + name: require('./package').name, /** - * The included hook runs once when the addon is loaded. - * We use it to generate extension files directly to the app directory. + * Hook that runs when the addon is included in the build */ included(app) { this._super.included.apply(this, arguments); console.log('[fleetbase-extensions-generator] ========================================'); - console.log('[fleetbase-extensions-generator] included() hook called'); - console.log('[fleetbase-extensions-generator] Project root:', this.project.root); + console.log('[fleetbase-extensions-generator] Generating Fleetbase extension files...'); + console.log('[fleetbase-extensions-generator] ========================================'); - // Discover extensions and cache them - this._extensions = this.discoverExtensions(); - console.log('[fleetbase-extensions-generator] Found', this._extensions.length, 'extension(s)'); + // Discover extensions + console.log('[fleetbase-extensions-generator] Discovering extensions...'); + const extensions = discoverExtensions(this.project.root); + console.log('[fleetbase-extensions-generator] Found', extensions.length, 'extension(s)'); - // Generate files directly to app directory - this.generateExtensionShims(this._extensions); - this.generateExtensionLoaders(this._extensions); - this.generateRouter(this._extensions); - this.generateExtensionsManifest(this._extensions); + if (extensions.length === 0) { + console.log('[fleetbase-extensions-generator] No extensions found, skipping generation'); + console.log('[fleetbase-extensions-generator] ========================================'); + return; + } - // Set up file watching for extension.js files - this.setupFileWatching(); + // Generate all files + this.generateAllFiles(extensions); + + // Watch for changes in development + watchExtensions(this.project.root, extensions, () => { + this.generateAllFiles(extensions); + }); console.log('[fleetbase-extensions-generator] ========================================'); }, /** - * Set up file watching for extension.js files to regenerate on changes + * Generate all extension files */ - setupFileWatching() { - if (this.app.env !== 'development') { - return; // Only watch in development - } - - const chokidar = require('chokidar'); - const extensionPaths = []; - - // Collect all extension.js file paths - for (const extension of this._extensions) { - const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js'); - - if (fs.existsSync(extensionPath)) { - extensionPaths.push(extensionPath); - } - } - - if (extensionPaths.length === 0) { - return; - } - - console.log('[fleetbase-extensions-generator] Watching', extensionPaths.length, 'extension file(s) for changes'); - - // Watch extension files - const watcher = chokidar.watch(extensionPaths, { - persistent: true, - ignoreInitial: true, - }); - - watcher.on('change', (changedPath) => { - console.log('[fleetbase-extensions-generator] Extension file changed:', changedPath); - console.log('[fleetbase-extensions-generator] Regenerating extension files...'); - - // Regenerate all extension files - this.generateExtensionShims(this._extensions); - this.generateExtensionLoaders(this._extensions); - - console.log('[fleetbase-extensions-generator] ✓ Regeneration complete'); - }); - }, - - /** - * Discover Fleetbase extensions from node_modules - */ - discoverExtensions() { - console.log('[fleetbase-extensions-generator] Discovering extensions...'); - - const extensions = []; - const seenPackages = new Set(); - - const results = fg.sync(['node_modules/*/package.json', 'node_modules/*/*/package.json'], { - cwd: this.project.root, - absolute: true, - }); - - for (const packagePath of results) { - let packageData = null; - - try { - const packageJson = fs.readFileSync(packagePath, 'utf8'); - packageData = JSON.parse(packageJson); - } catch (e) { - continue; - } - - // Check if this is a Fleetbase extension - if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) { - continue; - } - - // Skip duplicates - if (seenPackages.has(packageData.name)) { - continue; - } - - seenPackages.add(packageData.name); - - const extension = { - name: packageData.name, - version: packageData.version, - fleetbase: packageData.fleetbase || {}, - }; - - extensions.push(extension); - console.log('[fleetbase-extensions-generator] -', extension.name + '@' + extension.version); - } - - return extensions; - }, - - /** - * Generate extension shim files in app/extensions/ - */ - generateExtensionShims(extensions) { - console.log('[fleetbase-extensions-generator] Generating extension shims...'); - - const extensionsDir = path.join(this.project.root, 'app', 'extensions'); - - // Create directory - if (!fs.existsSync(extensionsDir)) { - fs.mkdirSync(extensionsDir, { recursive: true }); - } - - let shimCount = 0; - - for (const extension of extensions) { - const pkgName = extension.name; - const mountName = this.getExtensionMountPath(pkgName); - - // Path to extension.js in the engine's addon directory - const extensionPath = path.join(this.project.root, 'node_modules', pkgName, 'addon', 'extension.js'); - - // Check if extension.js exists - if (!fs.existsSync(extensionPath)) { - console.log('[fleetbase-extensions-generator] ! No extension.js found for', pkgName); - continue; - } - - // Read the extension code - let extensionCode; - try { - extensionCode = fs.readFileSync(extensionPath, 'utf8'); - } catch (error) { - console.error('[fleetbase-extensions-generator] ! Failed to read extension.js for', pkgName, ':', error.message); - continue; - } - - // Generate shim content - const shimContent = `// GENERATED BY fleetbase-extensions-generator - DO NOT EDIT -// Extension setup for ${pkgName} -// -// This file contains the inlined extension setup code from the engine. -// It is generated at build time to enable dynamic import without loading -// the entire engine bundle. - -${extensionCode} -`; - - // Write shim file - const shimPath = path.join(extensionsDir, `${mountName}.js`); - try { - fs.writeFileSync(shimPath, shimContent, 'utf8'); - shimCount++; - console.log('[fleetbase-extensions-generator] \u2713 Generated', `app/extensions/${mountName}.js`); - } catch (error) { - console.error('[fleetbase-extensions-generator] ! Failed to write shim for', pkgName, ':', error.message); - } - } - - console.log('[fleetbase-extensions-generator] Generated', shimCount, 'extension shim(s)'); - }, - - /** - * Generate extension loaders in app/utils/ - */ - generateExtensionLoaders(extensions) { - console.log('[fleetbase-extensions-generator] Generating extension loaders...'); - - const utilsDir = path.join(this.project.root, 'app', 'utils'); - - // Create directory - if (!fs.existsSync(utilsDir)) { - fs.mkdirSync(utilsDir, { recursive: true }); - } - - const lines = ['// GENERATED BY fleetbase-extensions-generator - DO NOT EDIT', '// Extension loader map for dynamic imports', '', 'export const EXTENSION_LOADERS = {']; - - let loaderCount = 0; - - for (const extension of extensions) { - const pkgName = extension.name; - - // Check if extension.js exists - const extensionPath = path.join(this.project.root, 'node_modules', pkgName, 'addon', 'extension.js'); - - if (!fs.existsSync(extensionPath)) { - continue; - } - - const mountName = this.getExtensionMountPath(pkgName); - lines.push(` '${pkgName}': () => import('@fleetbase/console/extensions/${mountName}'),`); - loaderCount++; - } - - lines.push('};'); - lines.push(''); - - const loadersContent = lines.join('\n'); - const loadersPath = path.join(utilsDir, 'extension-loaders.generated.js'); - - try { - fs.writeFileSync(loadersPath, loadersContent, 'utf8'); - console.log('[fleetbase-extensions-generator] \u2713 Generated app/utils/extension-loaders.generated.js with', loaderCount, 'loader(s)'); - } catch (error) { - console.error('[fleetbase-extensions-generator] ! Failed to write extension loaders:', error.message); - } - }, - - /** - * Generate router.js with extension mounts - */ - generateRouter(extensions) { - console.log('[fleetbase-extensions-generator] Generating router.js...'); - - const routerMapFile = path.join(this.project.root, 'router.map.js'); - const routerFile = path.join(this.project.root, 'app', 'router.js'); - - if (!fs.existsSync(routerMapFile)) { - console.error('[fleetbase-extensions-generator] ! router.map.js not found at:', routerMapFile); - return; - } - - // Read router.map.js (source template) - const routerContent = fs.readFileSync(routerMapFile, 'utf8'); - - // Separate extensions by mount location - const consoleExtensions = []; - const rootExtensions = []; - - for (const extension of extensions) { - const mountLocation = extension.fleetbase?.route?.mountLocation || 'console'; - const route = extension.fleetbase?.route?.slug || this.getExtensionMountPath(extension.name); - - if (mountLocation === 'console') { - consoleExtensions.push({ name: extension.name, route }); - } else if (mountLocation === 'root') { - rootExtensions.push({ name: extension.name, route }); - } - } - - console.log('[fleetbase-extensions-generator] Console extensions:', consoleExtensions.length); - console.log('[fleetbase-extensions-generator] Root extensions:', rootExtensions.length); - - // Parse and modify the router using simple string manipulation - // (We'll use recast for proper AST manipulation) - const recast = require('recast'); - const babelParser = require('recast/parsers/babel'); - - const ast = recast.parse(routerContent, { parser: babelParser }); - - let consoleAdded = 0; - let rootAdded = 0; - - // Add console extensions - if (consoleExtensions.length > 0) { - consoleAdded = this.addConsoleExtensions(ast, consoleExtensions); - } - - // Add root extensions - if (rootExtensions.length > 0) { - rootAdded = this.addRootExtensions(ast, rootExtensions); - } - - // Generate output - const output = recast.print(ast, { quote: 'single' }).code; - - try { - fs.writeFileSync(routerFile, output, 'utf8'); - console.log('[fleetbase-extensions-generator] \u2713 Generated app/router.js'); - console.log('[fleetbase-extensions-generator] - Console mounts:', consoleAdded); - console.log('[fleetbase-extensions-generator] - Root mounts:', rootAdded); - } catch (error) { - console.error('[fleetbase-extensions-generator] ! Failed to write router.js:', error.message); - } - }, - - /** - * Add console extensions to the router AST - */ - addConsoleExtensions(ast, extensions) { - const recast = require('recast'); - const types = recast.types; - const n = types.namedTypes; - const b = types.builders; - - let addedCount = 0; - - types.visit(ast, { - visitCallExpression(path) { - const node = path.node; - - // Look for this.route('console', ...) with path: '/' - if ( - n.MemberExpression.check(node.callee) && - n.ThisExpression.check(node.callee.object) && - node.callee.property.name === 'route' && - node.arguments.length > 0 && - n.Literal.check(node.arguments[0]) && - node.arguments[0].value === 'console' && - node.arguments.length > 1 && - n.ObjectExpression.check(node.arguments[1]) && - node.arguments[1].properties.some((p) => n.Property.check(p) && p.key.name === 'path' && n.Literal.check(p.value) && p.value.value === '/') - ) { - // Find the function expression in the third argument (after path config) - if (node.arguments.length > 2 && n.FunctionExpression.check(node.arguments[2])) { - const functionExpression = node.arguments[2]; - - // Add mount statements for each extension - extensions.forEach((extension) => { - // Check if already mounted - if (!this.isEngineMounted(functionExpression.body.body, extension.name)) { - const mountStatement = b.expressionStatement( - b.callExpression(b.memberExpression(b.thisExpression(), b.identifier('mount')), [ - b.literal(extension.name), - b.objectExpression([b.property('init', b.identifier('as'), b.literal(extension.route))]), - ]) - ); - - functionExpression.body.body.push(mountStatement); - addedCount++; - } - }); - } - - return false; // Don't traverse children - } - - this.traverse(path); - }, - }); - - return addedCount; - }, - - /** - * Add root extensions to the router AST - */ - addRootExtensions(ast, extensions) { - const recast = require('recast'); - const types = recast.types; - const n = types.namedTypes; - const b = types.builders; - - let addedCount = 0; - - types.visit(ast, { - visitCallExpression: (path) => { - const node = path.node; - - // Look for Router.map(function() { ... }) - if ( - n.MemberExpression.check(node.callee) && - n.Identifier.check(node.callee.object) && - node.callee.object.name === 'Router' && - node.callee.property.name === 'map' && - node.arguments.length > 0 && - n.FunctionExpression.check(node.arguments[0]) - ) { - const functionExpression = node.arguments[0]; - - // Add mount statements for each root extension - extensions.forEach((extension) => { - // Check if already mounted - if (!this.isEngineMounted(functionExpression.body.body, extension.name)) { - const mountStatement = b.expressionStatement( - b.callExpression(b.memberExpression(b.thisExpression(), b.identifier('mount')), [ - b.literal(extension.name), - b.objectExpression([b.property('init', b.identifier('as'), b.literal(extension.route))]), - ]) - ); - - functionExpression.body.body.push(mountStatement); - addedCount++; - } - }); - - return false; // Don't traverse children - } - - this.traverse(path); - }, - }); - - return addedCount; - }, - - /** - * Check if an engine is already mounted in the AST - */ - isEngineMounted(statements, engineName) { - const recast = require('recast'); - const types = recast.types; - const n = types.namedTypes; - - for (const statement of statements) { - if ( - n.ExpressionStatement.check(statement) && - n.CallExpression.check(statement.expression) && - n.MemberExpression.check(statement.expression.callee) && - statement.expression.callee.property.name === 'mount' && - statement.expression.arguments.length > 0 && - n.Literal.check(statement.expression.arguments[0]) && - statement.expression.arguments[0].value === engineName - ) { - return true; - } - } - - return false; - }, - - /** - * Generate extensions.json manifest in public/ - */ - generateExtensionsManifest(extensions) { - console.log('[fleetbase-extensions-generator] Generating extensions manifest...'); - - const publicDir = path.join(this.project.root, 'public'); - - // Create directory - if (!fs.existsSync(publicDir)) { - fs.mkdirSync(publicDir, { recursive: true }); - } - - const manifestPath = path.join(publicDir, 'extensions.json'); - const manifestContent = JSON.stringify(extensions, null, 2); - - try { - fs.writeFileSync(manifestPath, manifestContent, 'utf8'); - console.log('[fleetbase-extensions-generator] \u2713 Generated public/extensions.json'); - } catch (error) { - console.error('[fleetbase-extensions-generator] ! Failed to write extensions.json:', error.message); - } - }, - - /** - * Extract the mount path from an extension package name - */ - getExtensionMountPath(extensionName) { - const segments = extensionName.split('/'); - let mountName = segments[1] || segments[0]; - return mountName.replace('-engine', ''); + generateAllFiles(extensions) { + generateExtensionShims(this.project.root, extensions); + generateExtensionLoaders(this.project.root, extensions); + generateRouter(this.project.root, extensions); + generateExtensionsManifest(this.project.root, extensions); }, }; diff --git a/console/lib/fleetbase-extensions-generator/utils/discover-extensions.js b/console/lib/fleetbase-extensions-generator/utils/discover-extensions.js new file mode 100644 index 00000000..c1e69b3a --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/discover-extensions.js @@ -0,0 +1,39 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +/** + * Discover Fleetbase extensions from node_modules + */ +function discoverExtensions(projectRoot) { + const extensions = []; + const nodeModulesPath = path.join(projectRoot, 'node_modules'); + + if (!fs.existsSync(nodeModulesPath)) { + return extensions; + } + + // Find all package.json files in node_modules + const packageFiles = glob.sync('*/package.json', { + cwd: nodeModulesPath, + absolute: true, + }); + + for (const packageFile of packageFiles) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageFile, 'utf8')); + + // Check if it's a Fleetbase extension + if (packageJson.keywords && packageJson.keywords.includes('fleetbase-extension')) { + extensions.push(packageJson); + console.log('[fleetbase-extensions-generator] -', packageJson.name + '@' + packageJson.version); + } + } catch (error) { + // Skip invalid package.json files + } + } + + return extensions; +} + +module.exports = discoverExtensions; diff --git a/console/lib/fleetbase-extensions-generator/utils/generate-extension-loaders.js b/console/lib/fleetbase-extensions-generator/utils/generate-extension-loaders.js new file mode 100644 index 00000000..1a3e8a28 --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/generate-extension-loaders.js @@ -0,0 +1,62 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Get the mount path for an extension from its package name + */ +function getExtensionMountPath(packageName) { + return packageName.replace('@fleetbase/', '').replace('-engine', ''); +} + +/** + * Generate extension loaders map in app/utils/extension-loaders.generated.js + */ +function generateExtensionLoaders(projectRoot, extensions) { + console.log('[fleetbase-extensions-generator] Generating extension loaders...'); + + const utilsDir = path.join(projectRoot, 'app', 'utils'); + + // Create utils directory if it doesn't exist + if (!fs.existsSync(utilsDir)) { + fs.mkdirSync(utilsDir, { recursive: true }); + } + + // Build the loaders map + const loaders = {}; + + for (const extension of extensions) { + const extensionPath = path.join(projectRoot, 'node_modules', extension.name, 'addon', 'extension.js'); + + if (!fs.existsSync(extensionPath)) { + continue; + } + + const mountName = extension.fleetbase?.route?.slug || getExtensionMountPath(extension.name); + loaders[extension.name] = `() => import('@fleetbase/console/extensions/${mountName}')`; + } + + // Generate the loaders file + const loadersContent = `// Auto-generated extension loaders +// This file is generated by fleetbase-extensions-generator +// DO NOT EDIT MANUALLY + +export const EXTENSION_LOADERS = { +${Object.entries(loaders) + .map(([name, loader]) => ` '${name}': ${loader}`) + .join(',\n')} +}; + +export default EXTENSION_LOADERS; +`; + + const loadersFile = path.join(utilsDir, 'extension-loaders.generated.js'); + + try { + fs.writeFileSync(loadersFile, loadersContent, 'utf8'); + console.log('[fleetbase-extensions-generator] ✓ Generated app/utils/extension-loaders.generated.js with', Object.keys(loaders).length, 'loader(s)'); + } catch (error) { + console.error('[fleetbase-extensions-generator] ! Failed to write extension loaders:', error.message); + } +} + +module.exports = generateExtensionLoaders; diff --git a/console/lib/fleetbase-extensions-generator/utils/generate-extension-shims.js b/console/lib/fleetbase-extensions-generator/utils/generate-extension-shims.js new file mode 100644 index 00000000..6f0056d7 --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/generate-extension-shims.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Get the mount path for an extension from its package name + */ +function getExtensionMountPath(packageName) { + return packageName.replace('@fleetbase/', '').replace('-engine', ''); +} + +/** + * Generate extension shim files in app/extensions/ + */ +function generateExtensionShims(projectRoot, extensions) { + console.log('[fleetbase-extensions-generator] Generating extension shims...'); + + const extensionsDir = path.join(projectRoot, 'app', 'extensions'); + + // Create extensions directory if it doesn't exist + if (!fs.existsSync(extensionsDir)) { + fs.mkdirSync(extensionsDir, { recursive: true }); + } + + let generatedCount = 0; + + for (const extension of extensions) { + const extensionPath = path.join(projectRoot, 'node_modules', extension.name, 'addon', 'extension.js'); + + if (!fs.existsSync(extensionPath)) { + continue; + } + + // Read the extension.js content + const extensionContent = fs.readFileSync(extensionPath, 'utf8'); + + // Get mount name + const mountName = extension.fleetbase?.route?.slug || getExtensionMountPath(extension.name); + + // Generate the shim file that inlines the extension code + const shimContent = `// Auto-generated extension shim for ${extension.name} +// This file is generated by fleetbase-extensions-generator +// DO NOT EDIT MANUALLY + +${extensionContent} +`; + + const shimFile = path.join(extensionsDir, `${mountName}.js`); + + try { + fs.writeFileSync(shimFile, shimContent, 'utf8'); + console.log('[fleetbase-extensions-generator] ✓ Generated app/extensions/' + mountName + '.js'); + generatedCount++; + } catch (error) { + console.error('[fleetbase-extensions-generator] ! Failed to write extension shim:', error.message); + } + } + + console.log('[fleetbase-extensions-generator] Generated', generatedCount, 'extension shim(s)'); +} + +module.exports = generateExtensionShims; diff --git a/console/lib/fleetbase-extensions-generator/utils/generate-manifest.js b/console/lib/fleetbase-extensions-generator/utils/generate-manifest.js new file mode 100644 index 00000000..b8b15657 --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/generate-manifest.js @@ -0,0 +1,34 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Generate extensions.json manifest in public/ + */ +function generateExtensionsManifest(projectRoot, extensions) { + console.log('[fleetbase-extensions-generator] Generating extensions manifest...'); + + const publicDir = path.join(projectRoot, 'public'); + + // Create public directory if it doesn't exist + if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }); + } + + // Build the manifest + const manifest = extensions.map((ext) => ({ + name: ext.name, + version: ext.version, + route: ext.fleetbase?.route, + })); + + const manifestFile = path.join(publicDir, 'extensions.json'); + + try { + fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8'); + console.log('[fleetbase-extensions-generator] ✓ Generated public/extensions.json'); + } catch (error) { + console.error('[fleetbase-extensions-generator] ! Failed to write extensions manifest:', error.message); + } +} + +module.exports = generateExtensionsManifest; diff --git a/console/lib/fleetbase-extensions-generator/utils/generate-router.js b/console/lib/fleetbase-extensions-generator/utils/generate-router.js new file mode 100644 index 00000000..d0400856 --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/generate-router.js @@ -0,0 +1,209 @@ +const fs = require('fs'); +const path = require('path'); +const recast = require('recast'); +const babelParser = require('recast/parsers/babel'); + +/** + * Get the mount path for an extension from its package name + */ +function getExtensionMountPath(packageName) { + return packageName.replace('@fleetbase/', '').replace('-engine', ''); +} + +/** + * Check if an engine is already mounted in the AST + */ +function isEngineMounted(statements, engineName) { + const types = recast.types; + const n = types.namedTypes; + + for (const statement of statements) { + if ( + n.ExpressionStatement.check(statement) && + n.CallExpression.check(statement.expression) && + n.MemberExpression.check(statement.expression.callee) && + statement.expression.callee.property.name === 'mount' && + statement.expression.arguments.length > 0 && + n.Literal.check(statement.expression.arguments[0]) && + statement.expression.arguments[0].value === engineName + ) { + return true; + } + } + + return false; +} + +/** + * Add console extensions to the router AST + */ +function addConsoleExtensions(ast, extensions) { + const types = recast.types; + const n = types.namedTypes; + const b = types.builders; + + let addedCount = 0; + + types.visit(ast, { + visitCallExpression(path) { + const node = path.node; + + // Look for this.route('console', ...) with path: '/' + if ( + n.MemberExpression.check(node.callee) && + n.ThisExpression.check(node.callee.object) && + node.callee.property.name === 'route' && + node.arguments.length > 0 && + n.Literal.check(node.arguments[0]) && + node.arguments[0].value === 'console' && + node.arguments.length > 1 && + n.ObjectExpression.check(node.arguments[1]) && + node.arguments[1].properties.some((p) => n.Property.check(p) && p.key.name === 'path' && n.Literal.check(p.value) && p.value.value === '/') + ) { + // Find the function expression in the third argument (after path config) + if (node.arguments.length > 2 && n.FunctionExpression.check(node.arguments[2])) { + const functionExpression = node.arguments[2]; + + // Add mount statements for each extension + extensions.forEach((extension) => { + // Check if already mounted + if (!isEngineMounted(functionExpression.body.body, extension.name)) { + const mountStatement = b.expressionStatement( + b.callExpression(b.memberExpression(b.thisExpression(), b.identifier('mount')), [ + b.literal(extension.name), + b.objectExpression([b.property('init', b.identifier('as'), b.literal(extension.route))]), + ]) + ); + + functionExpression.body.body.push(mountStatement); + addedCount++; + } + }); + } + + return false; // Don't traverse children + } + + this.traverse(path); + }, + }); + + return addedCount; +} + +/** + * Add root extensions to the router AST + */ +function addRootExtensions(ast, extensions) { + const types = recast.types; + const n = types.namedTypes; + const b = types.builders; + + let addedCount = 0; + + types.visit(ast, { + visitCallExpression: (path) => { + const node = path.node; + + // Look for Router.map(function() { ... }) + if ( + n.MemberExpression.check(node.callee) && + n.Identifier.check(node.callee.object) && + node.callee.object.name === 'Router' && + node.callee.property.name === 'map' && + node.arguments.length > 0 && + n.FunctionExpression.check(node.arguments[0]) + ) { + const functionExpression = node.arguments[0]; + + // Add mount statements for each root extension + extensions.forEach((extension) => { + // Check if already mounted + if (!isEngineMounted(functionExpression.body.body, extension.name)) { + const mountStatement = b.expressionStatement( + b.callExpression(b.memberExpression(b.thisExpression(), b.identifier('mount')), [ + b.literal(extension.name), + b.objectExpression([b.property('init', b.identifier('as'), b.literal(extension.route))]), + ]) + ); + + functionExpression.body.body.push(mountStatement); + addedCount++; + } + }); + + return false; // Don't traverse children + } + + this.traverse(path); + }, + }); + + return addedCount; +} + +/** + * Generate router.js with extension mounts + */ +function generateRouter(projectRoot, extensions) { + console.log('[fleetbase-extensions-generator] Generating router.js...'); + + const routerMapFile = path.join(projectRoot, 'router.map.js'); + const routerFile = path.join(projectRoot, 'app', 'router.js'); + + if (!fs.existsSync(routerMapFile)) { + console.error('[fleetbase-extensions-generator] ! router.map.js not found at:', routerMapFile); + return; + } + + // Read router.map.js (source template) + const routerContent = fs.readFileSync(routerMapFile, 'utf8'); + + // Separate extensions by mount location + const consoleExtensions = []; + const rootExtensions = []; + + for (const extension of extensions) { + const mountLocation = extension.fleetbase?.route?.mountLocation || 'console'; + const route = extension.fleetbase?.route?.slug || getExtensionMountPath(extension.name); + + if (mountLocation === 'console') { + consoleExtensions.push({ name: extension.name, route }); + } else if (mountLocation === 'root') { + rootExtensions.push({ name: extension.name, route }); + } + } + + console.log('[fleetbase-extensions-generator] Console extensions:', consoleExtensions.length); + console.log('[fleetbase-extensions-generator] Root extensions:', rootExtensions.length); + + // Parse and modify the router using recast + const ast = recast.parse(routerContent, { parser: babelParser }); + + let consoleAdded = 0; + let rootAdded = 0; + + // Add console extensions + if (consoleExtensions.length > 0) { + consoleAdded = addConsoleExtensions(ast, consoleExtensions); + } + + // Add root extensions + if (rootExtensions.length > 0) { + rootAdded = addRootExtensions(ast, rootExtensions); + } + + // Generate output + const output = recast.print(ast, { quote: 'single' }).code; + + try { + fs.writeFileSync(routerFile, output, 'utf8'); + console.log('[fleetbase-extensions-generator] ✓ Generated app/router.js'); + console.log('[fleetbase-extensions-generator] - Console mounts:', consoleAdded); + console.log('[fleetbase-extensions-generator] - Root mounts:', rootAdded); + } catch (error) { + console.error('[fleetbase-extensions-generator] ! Failed to write router.js:', error.message); + } +} + +module.exports = generateRouter; diff --git a/console/lib/fleetbase-extensions-generator/utils/watch-extensions.js b/console/lib/fleetbase-extensions-generator/utils/watch-extensions.js new file mode 100644 index 00000000..6b513197 --- /dev/null +++ b/console/lib/fleetbase-extensions-generator/utils/watch-extensions.js @@ -0,0 +1,42 @@ +const path = require('path'); +const chokidar = require('chokidar'); + +/** + * Watch extension.js files for changes and regenerate on change + */ +function watchExtensions(projectRoot, extensions, regenerateCallback) { + const isDevelopment = process.env.EMBER_ENV !== 'production'; + + if (!isDevelopment) { + return; + } + + const extensionFiles = []; + + for (const extension of extensions) { + const extensionPath = path.join(projectRoot, 'node_modules', extension.name, 'addon', 'extension.js'); + extensionFiles.push(extensionPath); + } + + if (extensionFiles.length === 0) { + return; + } + + console.log('[fleetbase-extensions-generator] Watching', extensionFiles.length, 'extension file(s) for changes'); + + const watcher = chokidar.watch(extensionFiles, { + persistent: true, + ignoreInitial: true, + }); + + watcher.on('change', (filePath) => { + console.log('[fleetbase-extensions-generator] Extension file changed:', filePath); + console.log('[fleetbase-extensions-generator] Regenerating extension files...'); + regenerateCallback(); + console.log('[fleetbase-extensions-generator] ✓ Regeneration complete'); + }); + + return watcher; +} + +module.exports = watchExtensions;