@open-mova/cli 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,141 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ export const SHELL_REPOSITORY = 'https://github.com/rgarciadelongoria/open-mova.git';
6
+ const VERSION_PATTERN = /^v(\d+)\.(\d+)\.(\d+)$/;
7
+ const EXCLUDED_ENTRIES = new Set([
8
+ '.git',
9
+ '.angular',
10
+ 'node_modules',
11
+ 'dist',
12
+ 'out-tsc',
13
+ 'android',
14
+ 'ios',
15
+ 'AGENTS.md',
16
+ 'README.md',
17
+ ]);
18
+ export function listShellVersions() {
19
+ const output = runGit(['ls-remote', '--tags', '--refs', SHELL_REPOSITORY]);
20
+ return output
21
+ .split('\n')
22
+ .map((line) => line.match(/refs\/tags\/(v\d+\.\d+\.\d+)$/)?.[1])
23
+ .filter((version) => version !== undefined)
24
+ .sort(compareVersionsDescending);
25
+ }
26
+ export function downloadShell(destination, requestedVersion) {
27
+ return downloadTaggedProject('open-mova-shell', destination, requestedVersion);
28
+ }
29
+ export function downloadTaggedProject(project, destination, requestedVersion) {
30
+ const versions = listShellVersions();
31
+ const version = requestedVersion ?? versions[0];
32
+ if (!version) {
33
+ throw new Error('El repositorio no tiene versiones estables con tags vX.Y.Z.');
34
+ }
35
+ if (!VERSION_PATTERN.test(version) || !versions.includes(version)) {
36
+ throw new Error(`La versión ${version} no está disponible. Usa "mova shell versions".`);
37
+ }
38
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), 'open-mova-project-'));
39
+ try {
40
+ const checkout = join(temporaryDirectory, 'repository');
41
+ runGit([
42
+ '-c', 'advice.detachedHead=false',
43
+ 'clone', '--quiet', '--depth', '1', '--single-branch',
44
+ '--branch', version, SHELL_REPOSITORY, checkout,
45
+ ]);
46
+ const source = join(checkout, project);
47
+ const requiredFiles = project === 'open-mova-core'
48
+ ? ['package.json', 'src/index.ts']
49
+ : ['package.json', 'angular.json', 'src/main.ts'];
50
+ for (const requiredFile of requiredFiles) {
51
+ if (!existsSync(join(source, requiredFile))) {
52
+ throw new Error(`El tag ${version} no contiene ${project}: falta ${requiredFile}.`);
53
+ }
54
+ }
55
+ cpSync(source, destination, {
56
+ recursive: true,
57
+ filter: (entry) => !EXCLUDED_ENTRIES.has(basename(entry)),
58
+ });
59
+ return {
60
+ repository: SHELL_REPOSITORY,
61
+ version,
62
+ commit: runGit(['-C', checkout, 'rev-parse', 'HEAD']).trim(),
63
+ };
64
+ }
65
+ finally {
66
+ rmSync(temporaryDirectory, { recursive: true, force: true });
67
+ }
68
+ }
69
+ export function configureDownloadedShell(destination, applicationName, includesStarterMicrofrontend) {
70
+ const packagePath = join(destination, 'package.json');
71
+ const packageLockPath = join(destination, 'package-lock.json');
72
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
73
+ packageJson.name = applicationName;
74
+ packageJson.version = '0.1.0';
75
+ packageJson.private = true;
76
+ if (packageJson.dependencies?.['@open-mova/core']) {
77
+ packageJson.dependencies['@open-mova/core'] = 'file:packages/core';
78
+ }
79
+ writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
80
+ if (packageJson.dependencies?.['@open-mova/core']) {
81
+ // Las rutas file: cambian al copiar la shell fuera del monorepo.
82
+ rmSync(packageLockPath, { force: true });
83
+ }
84
+ else if (existsSync(packageLockPath)) {
85
+ const packageLock = JSON.parse(readFileSync(packageLockPath, 'utf8'));
86
+ packageLock.name = applicationName;
87
+ packageLock.version = packageJson.version;
88
+ packageLock.packages[''].name = applicationName;
89
+ packageLock.packages[''].version = packageJson.version;
90
+ writeFileSync(packageLockPath, `${JSON.stringify(packageLock, null, 2)}\n`);
91
+ }
92
+ const capacitorPath = join(destination, 'capacitor.config.ts');
93
+ if (existsSync(capacitorPath)) {
94
+ const originalConfig = readFileSync(capacitorPath, 'utf8');
95
+ const appId = `dev.openmova.app${applicationName.replaceAll('-', '')}`;
96
+ const customizedConfig = originalConfig
97
+ .replace(/(\bappId:\s*)['"][^'"]+['"]/, `$1'${appId}'`)
98
+ .replace(/(\bappName:\s*)['"][^'"]+['"]/, `$1'${applicationName}'`);
99
+ if (customizedConfig === originalConfig ||
100
+ !customizedConfig.includes(`appId: '${appId}'`) ||
101
+ !customizedConfig.includes(`appName: '${applicationName}'`)) {
102
+ throw new Error('No se pudo personalizar capacitor.config.ts de la shell descargada.');
103
+ }
104
+ writeFileSync(capacitorPath, customizedConfig);
105
+ }
106
+ writeFileSync(join(destination, '.nvmrc'), '22\n');
107
+ const usesCore = Boolean(packageJson.dependencies?.['@open-mova/core']);
108
+ const coreSteps = 'npm --prefix packages/core install\nnpm --prefix packages/core run build\n';
109
+ const librarySteps = usesCore || includesStarterMicrofrontend ? coreSteps : '';
110
+ const installSteps = `${librarySteps}npm install\n` +
111
+ (includesStarterMicrofrontend ? 'npm --prefix mfs/home install\n' : '');
112
+ const runSteps = includesStarterMicrofrontend
113
+ ? 'Inicia `npm --prefix mfs/home start` y `npm start` en dos terminales. Abre `http://localhost:4200/home/inicio`.\n'
114
+ : 'Registra primero un MF con `mova mf create nombre` y después ejecuta `mova start`.\n';
115
+ writeFileSync(join(destination, 'README.md'), `# ${applicationName}\n\nAplicación creada con Open Mova. \`mova.config.json\` registra la shell y los microfrontales remotos.\n\n` +
116
+ `Instala y compila las dependencias locales en este orden:\n\n` +
117
+ `\`\`\`bash\n${installSteps}\`\`\`\n\n${runSteps}`);
118
+ }
119
+ function compareVersionsDescending(first, second) {
120
+ const firstParts = VERSION_PATTERN.exec(first)?.slice(1).map(Number) ?? [];
121
+ const secondParts = VERSION_PATTERN.exec(second)?.slice(1).map(Number) ?? [];
122
+ for (let index = 0; index < 3; index += 1) {
123
+ const difference = (secondParts[index] ?? 0) - (firstParts[index] ?? 0);
124
+ if (difference !== 0)
125
+ return difference;
126
+ }
127
+ return 0;
128
+ }
129
+ function runGit(args) {
130
+ const result = spawnSync('git', args, {
131
+ encoding: 'utf8',
132
+ timeout: 120_000,
133
+ maxBuffer: 10 * 1024 * 1024,
134
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
135
+ });
136
+ if (result.error || result.status !== 0) {
137
+ const detail = result.stderr?.trim() || result.error?.message || 'Error desconocido';
138
+ throw new Error(`No se pudo consultar o descargar Open Mova: ${detail}`);
139
+ }
140
+ return result.stdout;
141
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { Command } from 'commander';
4
+ import { registerBuildCommand } from './commands/build.js';
5
+ import { registerCapacitorCommands } from './commands/capacitor.js';
6
+ import { registerCreateCommand } from './commands/create.js';
7
+ import { registerInfoCommand } from './commands/info.js';
8
+ import { registerMicrofrontendCommands } from './commands/microfrontend.js';
9
+ import { registerStartCommand } from './commands/start.js';
10
+ import { registerShellCommands } from './commands/shell.js';
11
+ const program = new Command();
12
+ const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
13
+ program
14
+ .name('mova')
15
+ .description('Herramientas de desarrollo para aplicaciones Open Mova')
16
+ .version(packageVersion.version);
17
+ registerCreateCommand(program);
18
+ registerMicrofrontendCommands(program);
19
+ registerStartCommand(program);
20
+ registerShellCommands(program);
21
+ registerBuildCommand(program);
22
+ registerCapacitorCommands(program);
23
+ registerInfoCommand(program);
24
+ try {
25
+ await program.parseAsync();
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : 'Error inesperado.';
29
+ console.error(`Error: ${message}`);
30
+ process.exitCode = 1;
31
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerBuildCommand(program: Command): void;
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
5
+ export function registerBuildCommand(program) {
6
+ program
7
+ .command('build')
8
+ .description('Compila los microfrontales locales y después la shell')
9
+ .action(() => {
10
+ const applicationRoot = requireApplicationRoot(process.cwd());
11
+ const configuration = readApplicationConfiguration(applicationRoot);
12
+ for (const microfrontend of configuration.microfrontends) {
13
+ if (!microfrontend.sourcePath) {
14
+ continue;
15
+ }
16
+ const projectDirectory = resolve(applicationRoot, microfrontend.sourcePath);
17
+ if (!existsSync(projectDirectory)) {
18
+ throw new Error(`No se encuentra el microfrontal "${microfrontend.name}" en ${projectDirectory}.`);
19
+ }
20
+ runBuild(projectDirectory, `microfrontal ${microfrontend.name}`);
21
+ }
22
+ runBuild(applicationRoot, 'shell');
23
+ });
24
+ }
25
+ function runBuild(directory, label) {
26
+ console.log(`Compilando ${label}...`);
27
+ const result = spawnSync('npm', ['run', 'build'], {
28
+ cwd: directory,
29
+ stdio: 'inherit',
30
+ });
31
+ if (result.status !== 0) {
32
+ throw new Error(`La compilación de ${label} no ha finalizado correctamente.`);
33
+ }
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCapacitorCommands(program: Command): void;
@@ -0,0 +1,84 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
5
+ export function registerCapacitorCommands(program) {
6
+ const capacitor = program.command('cap').description('Prepara la shell para Android o iOS');
7
+ capacitor.command('add <platform>')
8
+ .description('Crea el proyecto nativo de la plataforma')
9
+ .action((platform) => {
10
+ const selectedPlatform = parsePlatform(platform);
11
+ const root = requireApplicationRoot(process.cwd());
12
+ buildMobileShell(root);
13
+ runCapacitor(root, ['add', selectedPlatform]);
14
+ });
15
+ capacitor.command('sync [platform]')
16
+ .description('Compila la shell y sincroniza sus assets y plugins nativos')
17
+ .action((platform) => {
18
+ const root = requireApplicationRoot(process.cwd());
19
+ buildMobileShell(root);
20
+ runCapacitor(root, ['sync', ...(platform ? [parsePlatform(platform)] : [])]);
21
+ });
22
+ capacitor.command('open <platform>')
23
+ .description('Abre el proyecto nativo en Android Studio o Xcode')
24
+ .action((platform) => {
25
+ const root = requireApplicationRoot(process.cwd());
26
+ runCapacitor(root, ['open', parsePlatform(platform)]);
27
+ });
28
+ }
29
+ function parsePlatform(value) {
30
+ if (value !== 'android' && value !== 'ios') {
31
+ throw new Error('La plataforma debe ser android o ios.');
32
+ }
33
+ return value;
34
+ }
35
+ function buildMobileShell(root) {
36
+ if (!existsSync(join(root, 'capacitor.config.ts'))) {
37
+ throw new Error('Esta versión de shell no incluye Capacitor. Usa una shell v0.1.2 o posterior.');
38
+ }
39
+ const configuration = readApplicationConfiguration(root);
40
+ const manifest = createProductionManifest(configuration);
41
+ run(root, 'npm', ['run', 'build']);
42
+ const webDirectory = join(root, 'dist', 'browser');
43
+ if (!existsSync(join(webDirectory, 'index.html'))) {
44
+ throw new Error(`No se encuentra la shell compilada en ${webDirectory}.`);
45
+ }
46
+ // La compilación usa URLs locales para desarrollo; solo el artefacto móvil usa HTTPS.
47
+ writeFileSync(join(webDirectory, 'assets', 'federation.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
48
+ }
49
+ function createProductionManifest(configuration) {
50
+ return Object.fromEntries(configuration.microfrontends.map((microfrontend) => {
51
+ const entry = microfrontend.productionRemoteEntry;
52
+ if (!entry || !isHttpsUrl(entry)) {
53
+ throw new Error(`Configura productionRemoteEntry con HTTPS para el microfrontal "${microfrontend.name}" en mova.config.json.`);
54
+ }
55
+ return [microfrontend.remoteName, entry];
56
+ }));
57
+ }
58
+ function isHttpsUrl(value) {
59
+ try {
60
+ return new URL(value).protocol === 'https:';
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
66
+ function runCapacitor(root, args) {
67
+ if (!existsSync(join(root, 'capacitor.config.ts'))) {
68
+ throw new Error('Esta versión de shell no incluye Capacitor. Usa una shell v0.1.2 o posterior.');
69
+ }
70
+ const binary = join(root, 'node_modules', '.bin', 'cap');
71
+ if (!existsSync(binary)) {
72
+ throw new Error('Instala las dependencias de la aplicación con npm install.');
73
+ }
74
+ run(root, binary, args);
75
+ }
76
+ function run(root, command, args) {
77
+ const result = spawnSync(command, args, { cwd: root, stdio: 'inherit' });
78
+ if (result.error) {
79
+ throw result.error;
80
+ }
81
+ if (result.status !== 0) {
82
+ throw new Error(`${command} ${args.join(' ')} terminó con error.`);
83
+ }
84
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCreateCommand(program: Command): void;
@@ -0,0 +1,70 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { createMicrofrontend } from '../application/microfrontend.js';
4
+ import { addMicrofrontend, writeApplicationConfiguration, } from '../application/configuration.js';
5
+ import { synchronizeShellConfiguration } from '../application/shell-configuration.js';
6
+ import { configureDownloadedShell, downloadShell, downloadTaggedProject, } from '../application/shell-repository.js';
7
+ import { normalizeName } from '../utils/names.js';
8
+ export function registerCreateCommand(program) {
9
+ program
10
+ .command('create <name>')
11
+ .description('Crea una aplicación Open Mova con una shell y un microfrontal inicial')
12
+ .option('-d, --directory <path>', 'directorio donde crear la aplicación')
13
+ .option('--empty', 'no crear el microfrontal inicial')
14
+ .option('--shell-version <tag>', 'tag de la shell, por ejemplo v0.1.4')
15
+ .action((name, options) => {
16
+ const applicationName = normalizeName(name, 'El nombre de la aplicación');
17
+ const applicationRoot = resolve(options.directory ?? join(process.cwd(), applicationName));
18
+ if (existsSync(applicationRoot)) {
19
+ throw new Error(`Ya existe un directorio en ${applicationRoot}.`);
20
+ }
21
+ mkdirSync(dirname(applicationRoot), { recursive: true });
22
+ const temporaryApplication = mkdtempSync(join(dirname(applicationRoot), '.mova-create-'));
23
+ let shellVersion;
24
+ try {
25
+ const shell = downloadShell(temporaryApplication, options.shellVersion);
26
+ shellVersion = shell.version;
27
+ const shellPackage = JSON.parse(readFileSync(join(temporaryApplication, 'package.json'), 'utf8'));
28
+ const shellUsesCore = Boolean(shellPackage.dependencies?.['@open-mova/core']);
29
+ if (shellUsesCore || !options.empty) {
30
+ downloadTaggedProject('open-mova-core', join(temporaryApplication, 'packages/core'), shell.version);
31
+ }
32
+ configureDownloadedShell(temporaryApplication, applicationName, !options.empty);
33
+ let configuration = {
34
+ schemaVersion: 1,
35
+ name: applicationName,
36
+ shell,
37
+ microfrontends: [],
38
+ };
39
+ if (!options.empty) {
40
+ const starterMicrofrontend = createMicrofrontend(temporaryApplication, configuration, {
41
+ name: 'home',
42
+ directory: join('mfs', 'home'),
43
+ profile: 'demo',
44
+ templateVersion: shell.version,
45
+ });
46
+ configuration = addMicrofrontend(configuration, starterMicrofrontend);
47
+ }
48
+ writeApplicationConfiguration(temporaryApplication, configuration);
49
+ synchronizeShellConfiguration(temporaryApplication, configuration);
50
+ if (existsSync(applicationRoot)) {
51
+ throw new Error(`Ya existe un directorio en ${applicationRoot}.`);
52
+ }
53
+ renameSync(temporaryApplication, applicationRoot);
54
+ }
55
+ finally {
56
+ rmSync(temporaryApplication, { recursive: true, force: true });
57
+ }
58
+ console.log(`Aplicación creada en ${applicationRoot} con shell ${shellVersion}.`);
59
+ console.log('Instala las dependencias antes de iniciar el desarrollo:');
60
+ console.log(` cd ${applicationRoot}`);
61
+ const createdWithCore = existsSync(join(applicationRoot, 'packages/core'));
62
+ if (createdWithCore) {
63
+ console.log(' npm --prefix packages/core install && npm --prefix packages/core run build');
64
+ }
65
+ console.log(' npm install');
66
+ if (!options.empty) {
67
+ console.log(' npm --prefix mfs/home install');
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerInfoCommand(program: Command): void;
@@ -0,0 +1,26 @@
1
+ import { resolve } from 'node:path';
2
+ import { findApplicationRoot, readApplicationConfiguration, } from '../application/configuration.js';
3
+ export function registerInfoCommand(program) {
4
+ program
5
+ .command('info')
6
+ .description('Muestra la aplicación Open Mova encontrada desde la carpeta actual')
7
+ .action(() => {
8
+ const currentDirectory = resolve(process.cwd());
9
+ const applicationRoot = findApplicationRoot(currentDirectory);
10
+ console.log(`Directorio actual: ${currentDirectory}`);
11
+ if (!applicationRoot) {
12
+ console.log('Aplicación Open Mova: no encontrada');
13
+ return;
14
+ }
15
+ const configuration = readApplicationConfiguration(applicationRoot);
16
+ console.log(`Aplicación Open Mova: ${configuration.name}`);
17
+ console.log(`Raíz de la aplicación: ${applicationRoot}`);
18
+ if (configuration.shell) {
19
+ console.log(`Shell: ${configuration.shell.version} (${configuration.shell.commit.slice(0, 7)})`);
20
+ }
21
+ console.log(`Microfrontales registrados: ${configuration.microfrontends.length}`);
22
+ for (const microfrontend of configuration.microfrontends) {
23
+ console.log(`- ${microfrontend.name} → /${microfrontend.route}`);
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerMicrofrontendCommands(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createMicrofrontend, inspectExistingMicrofrontend } from '../application/microfrontend.js';
4
+ import { addMicrofrontend, readApplicationConfiguration, requireApplicationRoot, writeApplicationConfiguration, } from '../application/configuration.js';
5
+ import { synchronizeShellConfiguration } from '../application/shell-configuration.js';
6
+ import { normalizeName } from '../utils/names.js';
7
+ import { downloadTaggedProject, listShellVersions } from '../application/shell-repository.js';
8
+ export function registerMicrofrontendCommands(program) {
9
+ const microfrontend = program
10
+ .command('mf')
11
+ .description('Crea y registra microfrontales de una aplicación Open Mova');
12
+ microfrontend
13
+ .command('create <name>')
14
+ .description('Crea un microfrontal local y lo registra en la aplicación')
15
+ .option('-d, --directory <path>', 'ruta relativa a la raíz de la aplicación')
16
+ .option('--route <path>', 'ruta pública en la shell')
17
+ .option('--port <number>', 'puerto de desarrollo', parsePort)
18
+ .option('--production-remote-entry <url>', 'URL HTTPS del remoto publicado')
19
+ .option('--template-version <tag>', 'tag del proyecto de microfrontal')
20
+ .option('--demo', 'usar el perfil con ejemplos en vez del mínimo')
21
+ .action((name, options) => {
22
+ const applicationRoot = requireApplicationRoot(process.cwd());
23
+ const configuration = readApplicationConfiguration(applicationRoot);
24
+ const normalizedName = normalizeName(name, 'El nombre del microfrontal');
25
+ const templateVersion = options.templateVersion ?? (options.demo ? configuration.shell?.version : undefined) ?? listShellVersions()[0];
26
+ if (!templateVersion) {
27
+ throw new Error('No hay tags estables disponibles para crear el microfrontal.');
28
+ }
29
+ if (options.demo && configuration.shell?.version !== templateVersion) {
30
+ throw new Error('El perfil demo debe usar el mismo tag que la shell para mantener compatible el contrato nativo.');
31
+ }
32
+ if (options.demo && !existsSync(join(applicationRoot, 'packages/core'))) {
33
+ downloadTaggedProject('open-mova-core', join(applicationRoot, 'packages/core'), templateVersion);
34
+ }
35
+ const microfrontendConfiguration = createMicrofrontend(applicationRoot, configuration, {
36
+ name: normalizedName,
37
+ directory: options.directory ?? join('mfs', normalizedName),
38
+ route: options.route,
39
+ port: options.port,
40
+ productionRemoteEntry: options.productionRemoteEntry,
41
+ templateVersion,
42
+ profile: options.demo ? 'demo' : 'minimal',
43
+ });
44
+ const updatedConfiguration = addMicrofrontend(configuration, microfrontendConfiguration);
45
+ writeApplicationConfiguration(applicationRoot, updatedConfiguration);
46
+ synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
47
+ console.log(`Microfrontal "${microfrontendConfiguration.name}" creado.`);
48
+ console.log(`Ruta pública: /${microfrontendConfiguration.route}`);
49
+ console.log(`Directorio: ${microfrontendConfiguration.sourcePath}`);
50
+ });
51
+ microfrontend
52
+ .command('add [source-path]')
53
+ .description('Registra un microfrontal local o uno ya desplegado')
54
+ .option('--name <name>', 'nombre del microfrontal')
55
+ .option('--route <path>', 'ruta pública en la shell')
56
+ .option('--remote <name>', 'nombre definido en federation.config.js')
57
+ .option('--remote-entry <url>', 'URL del remoteEntry.json')
58
+ .option('--port <number>', 'puerto de desarrollo para un proyecto local', parsePort)
59
+ .option('--production-remote-entry <url>', 'URL HTTPS del remoto publicado')
60
+ .action((sourcePath, options) => {
61
+ const applicationRoot = requireApplicationRoot(process.cwd());
62
+ const configuration = readApplicationConfiguration(applicationRoot);
63
+ const microfrontendConfiguration = inspectExistingMicrofrontend(applicationRoot, {
64
+ sourcePath,
65
+ name: options.name,
66
+ route: options.route,
67
+ remoteName: options.remote,
68
+ remoteEntry: options.remoteEntry,
69
+ port: options.port,
70
+ productionRemoteEntry: options.productionRemoteEntry,
71
+ });
72
+ const updatedConfiguration = addMicrofrontend(configuration, microfrontendConfiguration);
73
+ writeApplicationConfiguration(applicationRoot, updatedConfiguration);
74
+ synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
75
+ console.log(`Microfrontal "${microfrontendConfiguration.name}" registrado.`);
76
+ console.log(`Ruta pública: /${microfrontendConfiguration.route}`);
77
+ });
78
+ }
79
+ function parsePort(value) {
80
+ const port = Number(value);
81
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
82
+ throw new Error('El puerto debe ser un número entero entre 1 y 65535.');
83
+ }
84
+ return port;
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerShellCommands(program: Command): void;
@@ -0,0 +1,18 @@
1
+ import { listShellVersions } from '../application/shell-repository.js';
2
+ export function registerShellCommands(program) {
3
+ program
4
+ .command('shell')
5
+ .description('Consulta las versiones publicadas de la shell')
6
+ .command('versions')
7
+ .description('Muestra los tags estables disponibles en el repositorio')
8
+ .action(() => {
9
+ const versions = listShellVersions();
10
+ if (versions.length === 0) {
11
+ console.log('No hay versiones estables disponibles.');
12
+ return;
13
+ }
14
+ for (const [index, version] of versions.entries()) {
15
+ console.log(index === 0 ? `${version} (última)` : version);
16
+ }
17
+ });
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerStartCommand(program: Command): void;
@@ -0,0 +1,69 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { resolve } from 'node:path';
4
+ import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
5
+ export function registerStartCommand(program) {
6
+ program
7
+ .command('start')
8
+ .description('Inicia la shell y los microfrontales locales registrados')
9
+ .option('--shell-only', 'inicia solo la shell')
10
+ .action(async (options) => {
11
+ const applicationRoot = requireApplicationRoot(process.cwd());
12
+ const configuration = readApplicationConfiguration(applicationRoot);
13
+ const projects = [];
14
+ if (!options.shellOnly) {
15
+ for (const microfrontend of configuration.microfrontends) {
16
+ if (!microfrontend.sourcePath) {
17
+ continue;
18
+ }
19
+ const projectDirectory = resolve(applicationRoot, microfrontend.sourcePath);
20
+ if (!existsSync(projectDirectory)) {
21
+ throw new Error(`No se encuentra el microfrontal "${microfrontend.name}" en ${projectDirectory}.`);
22
+ }
23
+ projects.push({
24
+ directory: projectDirectory,
25
+ label: `MF ${microfrontend.name}`,
26
+ });
27
+ }
28
+ }
29
+ projects.push({ directory: applicationRoot, label: 'Shell' });
30
+ console.log('Iniciando proyectos. Pulsa Ctrl+C para detenerlos.');
31
+ await startProjects(projects);
32
+ });
33
+ }
34
+ async function startProjects(projects) {
35
+ const children = projects.map(({ directory, label }) => {
36
+ console.log(`- ${label}: ${directory}`);
37
+ return spawn('npm', ['run', 'start'], {
38
+ cwd: directory,
39
+ stdio: 'inherit',
40
+ });
41
+ });
42
+ const stopChildren = () => {
43
+ for (const child of children) {
44
+ child.kill('SIGTERM');
45
+ }
46
+ };
47
+ const stopOnSignal = () => {
48
+ stopChildren();
49
+ };
50
+ process.once('SIGINT', stopOnSignal);
51
+ process.once('SIGTERM', stopOnSignal);
52
+ try {
53
+ const result = await Promise.race(children.map(waitForExit));
54
+ if (result.code !== 0 && result.signal === null) {
55
+ throw new Error('Uno de los servidores de desarrollo ha terminado con error.');
56
+ }
57
+ }
58
+ finally {
59
+ process.removeListener('SIGINT', stopOnSignal);
60
+ process.removeListener('SIGTERM', stopOnSignal);
61
+ stopChildren();
62
+ }
63
+ }
64
+ function waitForExit(child) {
65
+ return new Promise((resolve, reject) => {
66
+ child.once('error', reject);
67
+ child.once('exit', (code, signal) => resolve({ code, signal }));
68
+ });
69
+ }
@@ -0,0 +1,26 @@
1
+ export interface OpenMovaApplicationConfiguration {
2
+ readonly schemaVersion: 1;
3
+ readonly name: string;
4
+ readonly shell?: {
5
+ readonly repository: string;
6
+ readonly version: string;
7
+ readonly commit: string;
8
+ };
9
+ readonly microfrontends: readonly MicrofrontendConfiguration[];
10
+ }
11
+ export interface MicrofrontendConfiguration {
12
+ readonly name: string;
13
+ readonly route: string;
14
+ readonly remoteName: string;
15
+ readonly exposedModule: './Routes';
16
+ readonly developmentRemoteEntry: string;
17
+ readonly productionRemoteEntry?: string;
18
+ readonly sourcePath?: string;
19
+ readonly template?: {
20
+ readonly repository: string;
21
+ readonly version: string;
22
+ readonly commit: string;
23
+ readonly project: 'open-mova-mf-template';
24
+ readonly profile: 'minimal' | 'demo';
25
+ };
26
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare function normalizeName(value: string, label: string): string;
2
+ export declare function toDisplayName(value: string): string;
3
+ export declare function toPascalCase(value: string): string;
4
+ export declare function toRemoteName(name: string): string;