@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
448 lines (431 loc) • 15.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addMicrofrontend = addMicrofrontend;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const prompts_1 = __importDefault(require("prompts"));
const chalk_1 = __importDefault(require("chalk"));
/**
* Adds a new microfrontend to an existing Re-Shell project
*
* @param name - Name of the microfrontend
* @param options - Additional options for microfrontend creation
* @version 0.1.0
*/
async function addMicrofrontend(name, options) {
const { team, org = 're-shell', description = `${name} microfrontend for Re-Shell`, port = '5173', spinner, } = options;
// Normalize name to kebab-case for consistency
const normalizedName = name.toLowerCase().replace(/\s+/g, '-');
console.log(chalk_1.default.cyan(`Adding microfrontend "${normalizedName}"...`));
// Detect if we are in a Re-Shell project
const isInReshellProject = fs.existsSync('package.json') && (fs.existsSync('apps') || fs.existsSync('packages'));
if (!isInReshellProject) {
console.log(chalk_1.default.yellow("Warning: This doesn't appear to be a Re-Shell project. Creating standalone microfrontend."));
}
// Stop spinner for interactive prompts
if (spinner) {
spinner.stop();
}
// Ask for additional information if not provided
const responses = await (0, prompts_1.default)([
{
type: options.template ? null : 'select',
name: 'template',
message: 'Select a template:',
choices: [
{ title: 'React', value: 'react' },
{ title: 'React with TypeScript', value: 'react-ts' },
],
initial: 1, // Default to react-ts
},
{
type: options.route ? null : 'text',
name: 'route',
message: 'Route path for the microfrontend:',
initial: `/${normalizedName}`,
},
]);
// Merge responses with options
const finalOptions = {
...options,
template: options.template || responses.template,
route: options.route || responses.route,
};
// Restart spinner for file operations
if (spinner) {
spinner.start();
spinner.setText('Creating microfrontend files...');
}
// Determine microfrontend path based on project structure
let mfPath;
if (isInReshellProject && fs.existsSync('apps')) {
mfPath = path.resolve(process.cwd(), 'apps', normalizedName);
}
else {
mfPath = path.resolve(process.cwd(), normalizedName);
}
// Check if directory already exists and handle it gracefully
if (fs.existsSync(mfPath)) {
// Stop spinner for prompts
if (spinner) {
spinner.stop();
}
const { action } = await (0, prompts_1.default)({
type: 'select',
name: 'action',
message: `Directory "${normalizedName}" already exists. What would you like to do?`,
choices: [
{ title: 'Overwrite existing directory', value: 'overwrite' },
{ title: 'Cancel', value: 'cancel' },
],
initial: 0,
});
if (action === 'cancel') {
console.log(chalk_1.default.yellow('Operation cancelled.'));
return;
}
if (action === 'overwrite') {
// Restart spinner for file operations
if (spinner) {
spinner.start();
spinner.setText('Removing existing directory...');
}
await fs.remove(mfPath);
}
// Restart spinner for file operations
if (spinner) {
spinner.start();
spinner.setText('Creating microfrontend files...');
}
}
// Create directory structure
fs.mkdirSync(mfPath);
fs.mkdirSync(path.join(mfPath, 'src'));
fs.mkdirSync(path.join(mfPath, 'public'));
// Create package.json
const packageJson = {
name: isInReshellProject ? `@${org.toLowerCase()}/${normalizedName}` : normalizedName,
version: '0.1.0',
description,
main: 'dist/index.js',
scripts: {
dev: `vite --port ${port}`,
build: 'vite build',
preview: 'vite preview',
lint: 'eslint src --ext ts,tsx',
test: 'vitest',
},
dependencies: {
react: '^18.2.0',
'react-dom': '^18.2.0',
},
peerDependencies: {
'@re-shell/core': '^0.1.0',
},
devDependencies: {
vite: '^4.4.0',
'@vitejs/plugin-react': '^4.0.0',
eslint: '^8.44.0',
vitest: '^0.34.3',
...(finalOptions.template === 'react-ts'
? {
typescript: '^5.0.0',
'@types/react': '^18.2.0',
'@types/react-dom': '^18.2.0',
}
: {}),
},
keywords: ['microfrontend', 'react', 're-shell'],
author: team || org,
license: 'MIT',
reshell: {
type: 'microfrontend',
route: finalOptions.route,
},
};
fs.writeFileSync(path.join(mfPath, 'package.json'), JSON.stringify(packageJson, null, 2));
// Create vite.config.ts or vite.config.js
const fileExtension = finalOptions.template === 'react-ts' ? 'ts' : 'js';
const viteConfig = `
import { defineConfig } from 'vite';
import { resolve } from 'path';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: resolve(__dirname, 'src/index.${finalOptions.template === 'react-ts' ? 'tsx' : 'jsx'}'),
name: '${normalizedName.charAt(0).toUpperCase() +
normalizedName.slice(1).replace(/-./g, x => x[1].toUpperCase())}',
formats: ['umd'],
fileName: 'mf'
},
rollupOptions: {
external: ['react', 'react-dom', '@re-shell/core'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'@re-shell/core': 'ReShell'
}
}
}
},
server: {
port: ${port},
cors: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
});
`;
fs.writeFileSync(path.join(mfPath, `vite.config.${fileExtension}`), viteConfig);
// Create TypeScript configuration if using a TypeScript template
if (finalOptions.template === 'react-ts') {
const tsConfig = {
compilerOptions: {
target: 'ES2020',
useDefineForClassFields: true,
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
module: 'ESNext',
skipLibCheck: true,
moduleResolution: 'bundler',
allowImportingTsExtensions: true,
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: 'react-jsx',
strict: true,
noImplicitAny: true,
strictNullChecks: true,
},
include: ['src'],
references: [{ path: './tsconfig.node.json' }],
};
fs.writeFileSync(path.join(mfPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
const tsNodeConfig = {
compilerOptions: {
composite: true,
skipLibCheck: true,
module: 'ESNext',
moduleResolution: 'bundler',
allowSyntheticDefaultImports: true,
},
include: [`vite.config.${fileExtension}`],
};
fs.writeFileSync(path.join(mfPath, 'tsconfig.node.json'), JSON.stringify(tsNodeConfig, null, 2));
}
// Create main index file
const indexFileExtension = finalOptions.template === 'react-ts' ? 'tsx' : 'jsx';
const indexContent = `${finalOptions.template === 'react-ts'
? "import React from 'react';\nimport { createRoot } from 'react-dom/client';\n"
: "import { createRoot } from 'react-dom/client';\n"}import App from './App';
import { eventBus } from ${isInReshellProject ? "'@re-shell/core'" : './eventBus'};
// Entry point for the microfrontend
// This gets exposed when the script is loaded
window.${normalizedName.replace(/-./g, x => x[1].toUpperCase())} = {
mount: (containerId) => {
const container = document.getElementById(containerId);
if (!container) {
console.error(\`Container element with ID "\${containerId}" not found\`);
return;
}
// Using React 18's createRoot API
const root = createRoot(container);
root.render(<App />);
// Notify shell that microfrontend is loaded
eventBus.emit('microfrontend:loaded', { id: '${normalizedName}' });
// Store root for unmounting
window.${normalizedName.replace(/-./g, x => x[1].toUpperCase())}.root = root;
},
unmount: () => {
if (window.${normalizedName.replace(/-./g, x => x[1].toUpperCase())}.root) {
window.${normalizedName.replace(/-./g, x => x[1].toUpperCase())}.root.unmount();
}
}
};
// For development mode - mount the app immediately
if (process.env.NODE_ENV === 'development') {
const devRoot = document.getElementById('root');
if (devRoot) {
const root = createRoot(devRoot);
root.render(<App />);
}
}
export default App;
`;
fs.writeFileSync(path.join(mfPath, 'src', `index.${indexFileExtension}`), indexContent);
// Create app component
const appFileExtension = finalOptions.template === 'react-ts' ? 'tsx' : 'jsx';
const appContent = `${finalOptions.template === 'react-ts'
? "import React from 'react';\n\ninterface AppProps {}\n\n"
: ''}
function App(${finalOptions.template === 'react-ts' ? 'props: AppProps' : ''}) {
return (
<div className="${normalizedName}-app">
<h2>${normalizedName} Microfrontend</h2>
<p>This is a microfrontend created with Re-Shell CLI</p>
</div>
);
}
export default App;
`;
fs.writeFileSync(path.join(mfPath, 'src', `App.${appFileExtension}`), appContent);
// If not in a Re-Shell project, create eventBus file
if (!isInReshellProject) {
const eventBusFileExtension = finalOptions.template === 'react-ts' ? 'ts' : 'js';
const eventBusContent = `${finalOptions.template === 'react-ts'
? 'type EventHandler = (data: any) => void;\n\ninterface EventBus {\n events: Record<string, EventHandler[]>;\n on(event: string, callback: EventHandler): void;\n off(event: string, callback: EventHandler): void;\n emit(event: string, data: any): void;\n}\n\n'
: ''}
/**
* Simple event bus for communication between microfrontends
* In a real implementation, you would use the eventBus from @re-shell/core
*/
export const eventBus${finalOptions.template === 'react-ts' ? ': EventBus' : ''} = {
events: {},
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
},
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
},
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
};
`;
fs.writeFileSync(path.join(mfPath, 'src', `eventBus.${eventBusFileExtension}`), eventBusContent);
}
// Create HTML file for development mode
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${normalizedName} - Re-Shell Microfrontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.${finalOptions.template === 'react-ts' ? 'tsx' : 'jsx'}"></script>
</body>
</html>
`;
fs.writeFileSync(path.join(mfPath, 'public', 'index.html'), htmlContent);
// Create README.md
const readmeContent = `# ${normalizedName}
## Overview
This is a microfrontend for the Re-Shell architecture.
## Development
To start the development server:
\`\`\`bash
npm install
npm run dev
\`\`\`
## Building
To build the microfrontend:
\`\`\`bash
npm run build
\`\`\`
## Integration
This microfrontend can be integrated into a Re-Shell application by adding the following configuration to your shell application:
\`\`\`javascript
const microfrontendConfig = {
id: '${normalizedName}',
name: '${normalizedName.charAt(0).toUpperCase() +
normalizedName.slice(1).replace(/-./g, x => x[1].toUpperCase())}',
url: '/apps/${normalizedName}/dist/mf.umd.js', // Path to built bundle
containerId: '${normalizedName}-container',
route: '${finalOptions.route}',
team: '${team || 'Your Team'}'
};
\`\`\`
Then add the container element in your shell application:
\`\`\`jsx
<div id="${normalizedName}-container"></div>
\`\`\`
`;
fs.writeFileSync(path.join(mfPath, 'README.md'), readmeContent);
// Create .gitignore
const gitignoreContent = `# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
`;
fs.writeFileSync(path.join(mfPath, '.gitignore'), gitignoreContent);
// If part of a Re-Shell project and shell app exists, suggest shell integration
const shellAppPath = path.resolve(process.cwd(), 'apps', 'shell');
if (isInReshellProject &&
fs.existsSync(shellAppPath) &&
fs.existsSync(path.join(shellAppPath, 'src', 'App.tsx'))) {
console.log(chalk_1.default.cyan(`\nFound shell application at ${shellAppPath}`));
console.log(chalk_1.default.cyan(`Consider updating the shell application's configuration to include this microfrontend.`));
}
console.log(chalk_1.default.green(`\nMicrofrontend "${normalizedName}" created successfully at ${mfPath}`));
console.log('\nNext steps:');
console.log(` 1. cd ${isInReshellProject ? `apps/${normalizedName}` : normalizedName}`);
console.log(' 2. npm install (or your preferred package manager)');
console.log(' 3. npm run dev (to start development server)');
console.log(' 4. npm run build (to create production build)');
}