@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Open Mova contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/rgarciadelongoria/open-mova/main/assets/brand/open-mova-logo-title-rectangle.png" alt="Open Mova" width="720">
3
+ </p>
4
+
5
+ # Open Mova CLI
6
+
7
+ CLI de terminal para crear y mantener aplicaciones Open Mova. Se ejecuta desde la raíz de cada aplicación con el comando `mova`.
8
+
9
+ El CLI descarga la shell, el core y la plantilla de microfrontales desde tags estables del repositorio de Open Mova. No contiene copias de esas plantillas. Cada aplicación guarda en `mova.config.json` el tag y el commit con los que fue creada.
10
+
11
+ ## Índice de comandos
12
+
13
+ - [`mova create`](#mova-create)
14
+ - [`mova mf create`](#mova-mf-create)
15
+ - [`mova mf add`](#mova-mf-add)
16
+ - [`mova start`](#mova-start)
17
+ - [`mova build`](#mova-build)
18
+ - [`mova info`](#mova-info)
19
+ - [`mova shell versions`](#mova-shell-versions)
20
+ - [`mova cap add`](#mova-cap-add)
21
+ - [`mova cap sync`](#mova-cap-sync)
22
+ - [`mova cap open`](#mova-cap-open)
23
+
24
+ ## Instalación
25
+
26
+ Requiere Node.js 22 o posterior.
27
+
28
+ ```bash
29
+ npm install --global @open-mova/cli
30
+ mova --help
31
+ ```
32
+
33
+ ## Instalación para desarrollar el CLI
34
+
35
+ Requiere Node.js 22.
36
+
37
+ ```bash
38
+ cd open-mova-cli
39
+ npm install
40
+ npm run typecheck
41
+ npm run build
42
+ npm link
43
+ ```
44
+
45
+ `npm link` enlaza la versión local del comando `mova`; no publica el paquete.
46
+
47
+ ## Comandos
48
+
49
+ ### `mova create`
50
+
51
+ Crea una aplicación completa. Descarga una shell versionada, el core y un MF inicial `home`.
52
+
53
+ ```bash
54
+ mova create mi-aplicacion
55
+ mova create mi-aplicacion --directory ../apps/mi-aplicacion
56
+ mova create mi-aplicacion --shell-version v0.1.9
57
+ mova create mi-aplicacion --empty
58
+ ```
59
+
60
+ Sin `--shell-version` se usa el tag estable más reciente (`vMAJOR.MINOR.PATCH`). `--empty` omite el MF inicial.
61
+
62
+ ### `mova mf create`
63
+
64
+ Crea un microfrontal local, lo registra y actualiza la configuración de federación de la shell.
65
+
66
+ ```bash
67
+ cd mi-aplicacion
68
+ mova mf create catalog
69
+ mova mf create catalog --route productos --port 4500
70
+ mova mf create catalog --directory ../provider-mf-catalog
71
+ mova mf create catalog --demo
72
+ ```
73
+
74
+ Por defecto se crea en `mfs/catalog`, con perfil mínimo, ruta `/catalog` y un puerto libre desde `4300`. `--demo` añade ejemplos de Device y Camera. Se puede fijar la plantilla con `--template-version v0.1.9`.
75
+
76
+ ### `mova mf add`
77
+
78
+ Registra un microfrontal existente, sin crearlo de nuevo.
79
+
80
+ ```bash
81
+ mova mf add ../provider-mf-catalog
82
+ mova mf add ../provider-mf-catalog --name catalog --route productos \
83
+ --remote catalog-microfrontend --port 4500
84
+ ```
85
+
86
+ Para registrar solo un MF ya publicado:
87
+
88
+ ```bash
89
+ mova mf add --name catalog --route productos \
90
+ --remote catalog-microfrontend \
91
+ --remote-entry https://cdn.example.com/catalog/remoteEntry.json
92
+ ```
93
+
94
+ El CLI actualiza `mova.config.json`, `src/assets/federation.manifest.json` y `src/app/application.config.ts`.
95
+
96
+ ### `mova start`
97
+
98
+ Arranca los MFs locales registrados y después la shell. Los MFs configurados solo con una URL remota no se arrancan localmente.
99
+
100
+ ```bash
101
+ mova start
102
+ mova start --shell-only
103
+ ```
104
+
105
+ ### `mova build`
106
+
107
+ Compila los microfrontales locales y la shell.
108
+
109
+ ```bash
110
+ mova build
111
+ ```
112
+
113
+ ### `mova info`
114
+
115
+ Muestra la aplicación detectada, la versión de shell y sus microfrontales. También funciona desde subdirectorios como `mfs/catalog`.
116
+
117
+ ```bash
118
+ mova info
119
+ ```
120
+
121
+ ### `mova shell versions`
122
+
123
+ Lista los tags estables disponibles para crear aplicaciones.
124
+
125
+ ```bash
126
+ mova shell versions
127
+ ```
128
+
129
+ ### `mova cap add`
130
+
131
+ Compila la aplicación y añade una plataforma nativa. Configura antes el `appId` definitivo en `capacitor.config.ts`.
132
+
133
+ ```bash
134
+ mova cap add android
135
+ mova cap add ios
136
+ ```
137
+
138
+ ### `mova cap sync`
139
+
140
+ Sincroniza la shell compilada, los recursos y los plugins con la plataforma. Sin plataforma, sincroniza todas las plataformas añadidas.
141
+
142
+ ```bash
143
+ mova cap sync android
144
+ mova cap sync
145
+ ```
146
+
147
+ ### `mova cap open`
148
+
149
+ Abre el proyecto nativo en Android Studio o Xcode.
150
+
151
+ ```bash
152
+ mova cap open android
153
+ mova cap open ios
154
+ ```
155
+
156
+ ## Flujo habitual
157
+
158
+ ```bash
159
+ mova create mi-aplicacion
160
+ cd mi-aplicacion
161
+ npm install
162
+ npm --prefix packages/core install
163
+ npm --prefix packages/core run build
164
+ npm --prefix mfs/home install
165
+ mova start
166
+ ```
167
+
168
+ Los microfrontales siempre se cargan remotamente mediante Native Federation. En producción, cada MF debe estar publicado en HTTPS y registrado con su `productionRemoteEntry`. Capacitor no copia los MFs al paquete nativo: la shell los carga desde sus URLs en ejecución.
169
+
170
+ ## Limitaciones actuales
171
+
172
+ El CLI todavía no actualiza automáticamente la shell de una aplicación creada ni publica microfrontales. Los comandos móviles requieren una versión de shell que incluya Capacitor.
@@ -0,0 +1,7 @@
1
+ import type { MicrofrontendConfiguration, OpenMovaApplicationConfiguration } from '../types.js';
2
+ export declare const APPLICATION_CONFIGURATION_FILE = "mova.config.json";
3
+ export declare function findApplicationRoot(startDirectory: string): string | undefined;
4
+ export declare function requireApplicationRoot(startDirectory: string): string;
5
+ export declare function readApplicationConfiguration(applicationRoot: string): OpenMovaApplicationConfiguration;
6
+ export declare function writeApplicationConfiguration(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
7
+ export declare function addMicrofrontend(configuration: OpenMovaApplicationConfiguration, microfrontend: MicrofrontendConfiguration): OpenMovaApplicationConfiguration;
@@ -0,0 +1,135 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ export const APPLICATION_CONFIGURATION_FILE = 'mova.config.json';
4
+ export function findApplicationRoot(startDirectory) {
5
+ let currentDirectory = resolve(startDirectory);
6
+ while (true) {
7
+ if (existsSync(join(currentDirectory, APPLICATION_CONFIGURATION_FILE))) {
8
+ return currentDirectory;
9
+ }
10
+ const parentDirectory = dirname(currentDirectory);
11
+ if (parentDirectory === currentDirectory) {
12
+ return undefined;
13
+ }
14
+ currentDirectory = parentDirectory;
15
+ }
16
+ }
17
+ export function requireApplicationRoot(startDirectory) {
18
+ const applicationRoot = findApplicationRoot(startDirectory);
19
+ if (!applicationRoot) {
20
+ throw new Error(`No se ha encontrado ${APPLICATION_CONFIGURATION_FILE}. Ejecuta este comando desde una aplicación Open Mova.`);
21
+ }
22
+ return applicationRoot;
23
+ }
24
+ export function readApplicationConfiguration(applicationRoot) {
25
+ const configurationPath = join(applicationRoot, APPLICATION_CONFIGURATION_FILE);
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(configurationPath, 'utf8'));
28
+ return validateApplicationConfiguration(parsed, configurationPath);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error) {
32
+ throw error;
33
+ }
34
+ throw new Error(`No se ha podido leer ${configurationPath}.`);
35
+ }
36
+ }
37
+ export function writeApplicationConfiguration(applicationRoot, configuration) {
38
+ const configurationPath = join(applicationRoot, APPLICATION_CONFIGURATION_FILE);
39
+ const content = `${JSON.stringify(configuration, null, 2)}\n`;
40
+ writeFileSync(configurationPath, content, 'utf8');
41
+ }
42
+ export function addMicrofrontend(configuration, microfrontend) {
43
+ const routeInUse = configuration.microfrontends.some((entry) => entry.route === microfrontend.route);
44
+ const nameInUse = configuration.microfrontends.some((entry) => entry.name === microfrontend.name);
45
+ const remoteInUse = configuration.microfrontends.some((entry) => entry.remoteName === microfrontend.remoteName);
46
+ const remoteEntryInUse = configuration.microfrontends.some((entry) => entry.developmentRemoteEntry === microfrontend.developmentRemoteEntry);
47
+ if (nameInUse || routeInUse || remoteInUse || remoteEntryInUse) {
48
+ throw new Error(`El microfrontal "${microfrontend.name}" entra en conflicto con una entrada existente. ` +
49
+ 'El nombre, la ruta, el remoto y la URL de desarrollo deben ser únicos.');
50
+ }
51
+ return {
52
+ ...configuration,
53
+ microfrontends: [...configuration.microfrontends, microfrontend],
54
+ };
55
+ }
56
+ function validateApplicationConfiguration(value, configurationPath) {
57
+ if (!isRecord(value)) {
58
+ throw new Error(`${configurationPath} no contiene un objeto JSON válido.`);
59
+ }
60
+ if (value.schemaVersion !== 1 || typeof value.name !== 'string') {
61
+ throw new Error(`${configurationPath} no tiene el formato de Open Mova esperado.`);
62
+ }
63
+ if (!Array.isArray(value.microfrontends)) {
64
+ throw new Error(`${configurationPath} debe contener una lista de microfrontales.`);
65
+ }
66
+ let shell;
67
+ if (value.shell !== undefined) {
68
+ if (!isRecord(value.shell) ||
69
+ typeof value.shell.repository !== 'string' ||
70
+ typeof value.shell.version !== 'string' ||
71
+ typeof value.shell.commit !== 'string') {
72
+ throw new Error(`${configurationPath} contiene una versión de shell no válida.`);
73
+ }
74
+ shell = {
75
+ repository: value.shell.repository,
76
+ version: value.shell.version,
77
+ commit: value.shell.commit,
78
+ };
79
+ }
80
+ const microfrontends = value.microfrontends.map((entry) => validateMicrofrontend(entry, configurationPath));
81
+ return {
82
+ schemaVersion: 1,
83
+ name: value.name,
84
+ ...(shell ? { shell } : {}),
85
+ microfrontends,
86
+ };
87
+ }
88
+ function validateMicrofrontend(value, configurationPath) {
89
+ if (!isRecord(value)) {
90
+ throw new Error(`${configurationPath} contiene un microfrontal no válido.`);
91
+ }
92
+ const requiredStrings = [
93
+ value.name,
94
+ value.route,
95
+ value.remoteName,
96
+ value.exposedModule,
97
+ value.developmentRemoteEntry,
98
+ ];
99
+ if (requiredStrings.some((entry) => typeof entry !== 'string')) {
100
+ throw new Error(`${configurationPath} contiene un microfrontal incompleto.`);
101
+ }
102
+ if (value.exposedModule !== './Routes') {
103
+ throw new Error(`${configurationPath} solo admite "./Routes" como módulo expuesto por el momento.`);
104
+ }
105
+ if (value.sourcePath !== undefined && typeof value.sourcePath !== 'string') {
106
+ throw new Error(`${configurationPath} contiene una ruta de origen no válida.`);
107
+ }
108
+ if (value.productionRemoteEntry !== undefined &&
109
+ typeof value.productionRemoteEntry !== 'string') {
110
+ throw new Error(`${configurationPath} contiene una URL de producción no válida.`);
111
+ }
112
+ if (value.template !== undefined && (!isRecord(value.template) ||
113
+ typeof value.template.repository !== 'string' ||
114
+ typeof value.template.version !== 'string' ||
115
+ typeof value.template.commit !== 'string' ||
116
+ value.template.project !== 'open-mova-mf-template' ||
117
+ (value.template.profile !== 'minimal' && value.template.profile !== 'demo'))) {
118
+ throw new Error(`${configurationPath} contiene una plantilla de microfrontal no válida.`);
119
+ }
120
+ return {
121
+ name: value.name,
122
+ route: value.route,
123
+ remoteName: value.remoteName,
124
+ exposedModule: './Routes',
125
+ developmentRemoteEntry: value.developmentRemoteEntry,
126
+ ...(value.productionRemoteEntry === undefined
127
+ ? {}
128
+ : { productionRemoteEntry: value.productionRemoteEntry }),
129
+ ...(value.sourcePath === undefined ? {} : { sourcePath: value.sourcePath }),
130
+ ...(value.template === undefined ? {} : { template: value.template }),
131
+ };
132
+ }
133
+ function isRecord(value) {
134
+ return typeof value === 'object' && value !== null;
135
+ }
@@ -0,0 +1,2 @@
1
+ export type MicrofrontendProfile = 'minimal' | 'demo';
2
+ export declare function configureDownloadedMicrofrontend(destination: string, applicationRoot: string, name: string, port: number, profile: MicrofrontendProfile): void;
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { toDisplayName, toRemoteName } from '../utils/names.js';
4
+ export function configureDownloadedMicrofrontend(destination, applicationRoot, name, port, profile) {
5
+ const projectName = `mova-mf-${name}`;
6
+ const packagePath = join(destination, 'package.json');
7
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
8
+ packageJson.name = projectName;
9
+ packageJson.scripts.start = `ng serve ${projectName}`;
10
+ packageJson.scripts.build = `ng build ${projectName}`;
11
+ if (profile === 'minimal') {
12
+ delete packageJson.dependencies['@open-mova/core'];
13
+ writeFileSync(join(destination, 'src/app/app.routes.ts'), minimalRoutes(name));
14
+ writeFileSync(join(destination, 'src/app/app.config.ts'), minimalAppConfig());
15
+ }
16
+ else {
17
+ const core = join(applicationRoot, 'packages/core');
18
+ if (!existsSync(core)) {
19
+ throw new Error('El perfil demo requiere packages/core en la aplicación.');
20
+ }
21
+ packageJson.dependencies['@open-mova/core'] = `file:${relative(destination, core)}`;
22
+ }
23
+ writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`);
24
+ const angularPath = join(destination, 'angular.json');
25
+ const angular = readFileSync(angularPath, 'utf8')
26
+ .replaceAll('open-mova-mf-template', projectName)
27
+ .replace('"port": 4300', `"port": ${port}`);
28
+ writeFileSync(angularPath, angular);
29
+ const federationPath = join(destination, 'federation.config.js');
30
+ writeFileSync(federationPath, readFileSync(federationPath, 'utf8').replace('demo-microfrontend', toRemoteName(name)));
31
+ const indexPath = join(destination, 'src/index.html');
32
+ writeFileSync(indexPath, readFileSync(indexPath, 'utf8')
33
+ .replaceAll('Demo microfrontend', toDisplayName(name))
34
+ .replaceAll('mova-demo-microfrontend', `mova-${name}-microfrontend`));
35
+ const appPath = join(destination, 'src/app/app.ts');
36
+ writeFileSync(appPath, readFileSync(appPath, 'utf8').replace('mova-demo-microfrontend', `mova-${name}-microfrontend`));
37
+ // El lock heredado ya no representa el proyecto renombrado ni su dependencia local.
38
+ rmSync(join(destination, 'package-lock.json'), { force: true });
39
+ writeFileSync(join(destination, 'README.md'), `# ${projectName}\n\nMicrofrontal Open Mova (${profile}). Ejecuta \`npm install\` y \`npm start\`. La shell lo carga en http://localhost:${port}/remoteEntry.json.\n`);
40
+ }
41
+ function minimalRoutes(name) {
42
+ return `import { Component } from '@angular/core';
43
+ import { Routes } from '@angular/router';
44
+
45
+ @Component({
46
+ standalone: true,
47
+ template: '<h1>${toDisplayName(name)}</h1>',
48
+ })
49
+ export class HomeComponent {}
50
+
51
+ // Añade aquí las rutas y componentes propios de este microfrontal.
52
+ export const routes: Routes = [
53
+ { path: '', component: HomeComponent },
54
+ ];
55
+ `;
56
+ }
57
+ function minimalAppConfig() {
58
+ return `import { ApplicationConfig } from '@angular/core';
59
+ import { provideRouter } from '@angular/router';
60
+ import { routes } from './app.routes';
61
+
62
+ export const appConfig: ApplicationConfig = {
63
+ providers: [provideRouter(routes)],
64
+ };
65
+ `;
66
+ }
@@ -0,0 +1,22 @@
1
+ import type { MicrofrontendConfiguration, OpenMovaApplicationConfiguration } from '../types.js';
2
+ import { type MicrofrontendProfile } from './microfrontend-profile.js';
3
+ export interface CreateMicrofrontendOptions {
4
+ readonly name: string;
5
+ readonly directory: string;
6
+ readonly route?: string;
7
+ readonly port?: number;
8
+ readonly productionRemoteEntry?: string;
9
+ readonly profile?: MicrofrontendProfile;
10
+ readonly templateVersion?: string;
11
+ }
12
+ export interface ExistingMicrofrontendOptions {
13
+ readonly sourcePath?: string;
14
+ readonly name?: string;
15
+ readonly route?: string;
16
+ readonly remoteName?: string;
17
+ readonly remoteEntry?: string;
18
+ readonly productionRemoteEntry?: string;
19
+ readonly port?: number;
20
+ }
21
+ export declare function createMicrofrontend(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, options: CreateMicrofrontendOptions): MicrofrontendConfiguration;
22
+ export declare function inspectExistingMicrofrontend(applicationRoot: string, options: ExistingMicrofrontendOptions): MicrofrontendConfiguration;
@@ -0,0 +1,135 @@
1
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { normalizeName, toRemoteName } from '../utils/names.js';
4
+ import { configureDownloadedMicrofrontend } from './microfrontend-profile.js';
5
+ import { downloadTaggedProject } from './shell-repository.js';
6
+ export function createMicrofrontend(applicationRoot, configuration, options) {
7
+ const name = normalizeName(options.name, 'El nombre del microfrontal');
8
+ const route = normalizeName(options.route ?? name, 'La ruta del microfrontal');
9
+ const destination = resolve(applicationRoot, options.directory);
10
+ if (existsSync(destination)) {
11
+ throw new Error(`Ya existe un directorio en ${destination}.`);
12
+ }
13
+ const port = options.port ?? findAvailablePort(configuration);
14
+ validatePort(port);
15
+ const profile = options.profile ?? 'minimal';
16
+ let template;
17
+ try {
18
+ template = downloadTaggedProject('open-mova-mf-template', destination, options.templateVersion);
19
+ configureDownloadedMicrofrontend(destination, applicationRoot, name, port, profile);
20
+ }
21
+ catch (error) {
22
+ rmSync(destination, { recursive: true, force: true });
23
+ throw error;
24
+ }
25
+ return {
26
+ name,
27
+ route,
28
+ remoteName: toRemoteName(name),
29
+ exposedModule: './Routes',
30
+ developmentRemoteEntry: `http://localhost:${port}/remoteEntry.json`,
31
+ ...(options.productionRemoteEntry
32
+ ? { productionRemoteEntry: options.productionRemoteEntry }
33
+ : {}),
34
+ sourcePath: toConfigurationPath(applicationRoot, destination),
35
+ template: { ...template, project: 'open-mova-mf-template', profile },
36
+ };
37
+ }
38
+ export function inspectExistingMicrofrontend(applicationRoot, options) {
39
+ if (!options.sourcePath && !options.remoteEntry) {
40
+ throw new Error('Indica una ruta de proyecto o usa --remote-entry para registrar un microfrontal ya desplegado.');
41
+ }
42
+ const sourceDirectory = options.sourcePath
43
+ ? resolve(applicationRoot, options.sourcePath)
44
+ : undefined;
45
+ const discovered = sourceDirectory
46
+ ? inspectLocalProject(sourceDirectory)
47
+ : { remoteName: undefined, port: undefined };
48
+ const remoteName = options.remoteName ?? discovered.remoteName;
49
+ const inferredName = remoteName?.replace(/-microfrontend$/, '');
50
+ const name = normalizeName(options.name ?? inferredName ?? '', 'El nombre del microfrontal');
51
+ const route = normalizeName(options.route ?? name, 'La ruta del microfrontal');
52
+ const resolvedRemoteName = remoteName ?? toRemoteName(name);
53
+ const port = options.port ?? discovered.port;
54
+ if (port !== undefined) {
55
+ validatePort(port);
56
+ }
57
+ const remoteEntry = options.remoteEntry ??
58
+ (port === undefined ? undefined : `http://localhost:${port}/remoteEntry.json`);
59
+ if (!remoteEntry) {
60
+ throw new Error('No se ha podido detectar el puerto. Indica --remote-entry o --port.');
61
+ }
62
+ return {
63
+ name,
64
+ route,
65
+ remoteName: resolvedRemoteName,
66
+ exposedModule: './Routes',
67
+ developmentRemoteEntry: remoteEntry,
68
+ ...(options.productionRemoteEntry
69
+ ? { productionRemoteEntry: options.productionRemoteEntry }
70
+ : {}),
71
+ ...(sourceDirectory
72
+ ? { sourcePath: toConfigurationPath(applicationRoot, sourceDirectory) }
73
+ : {}),
74
+ };
75
+ }
76
+ function inspectLocalProject(directory) {
77
+ const federationConfiguration = resolve(directory, 'federation.config.js');
78
+ const angularConfiguration = resolve(directory, 'angular.json');
79
+ const packageConfiguration = resolve(directory, 'package.json');
80
+ if (!existsSync(federationConfiguration) ||
81
+ !existsSync(angularConfiguration) ||
82
+ !existsSync(packageConfiguration)) {
83
+ throw new Error(`${directory} no parece un microfrontal Open Mova. Debe contener package.json, angular.json y federation.config.js.`);
84
+ }
85
+ const federationContent = readFileSync(federationConfiguration, 'utf8');
86
+ const remoteName = federationContent.match(/\bname\s*:\s*['"]([^'"]+)['"]/)?.[1];
87
+ const port = readPort(angularConfiguration);
88
+ return { remoteName, port };
89
+ }
90
+ function readPort(configurationPath) {
91
+ const parsed = JSON.parse(readFileSync(configurationPath, 'utf8'));
92
+ if (!isRecord(parsed) || !isRecord(parsed.projects)) {
93
+ return undefined;
94
+ }
95
+ const firstProject = Object.values(parsed.projects)[0];
96
+ if (!isRecord(firstProject) || !isRecord(firstProject.architect)) {
97
+ return undefined;
98
+ }
99
+ const serveApplication = firstProject.architect['serve-application'];
100
+ if (!isRecord(serveApplication) || !isRecord(serveApplication.options)) {
101
+ return undefined;
102
+ }
103
+ const port = serveApplication.options.port;
104
+ return typeof port === 'number' ? port : undefined;
105
+ }
106
+ function findAvailablePort(configuration) {
107
+ const occupiedPorts = configuration.microfrontends
108
+ .map((microfrontend) => extractPort(microfrontend.developmentRemoteEntry))
109
+ .filter((port) => port !== undefined);
110
+ let candidate = 4300;
111
+ while (occupiedPorts.includes(candidate)) {
112
+ candidate += 100;
113
+ }
114
+ return candidate;
115
+ }
116
+ function extractPort(remoteEntry) {
117
+ try {
118
+ const port = new URL(remoteEntry).port;
119
+ return port === '' ? undefined : Number(port);
120
+ }
121
+ catch {
122
+ return undefined;
123
+ }
124
+ }
125
+ function validatePort(port) {
126
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
127
+ throw new Error('El puerto debe ser un número entre 1 y 65535.');
128
+ }
129
+ }
130
+ function toConfigurationPath(applicationRoot, targetPath) {
131
+ return relative(applicationRoot, targetPath).split(sep).join('/');
132
+ }
133
+ function isRecord(value) {
134
+ return typeof value === 'object' && value !== null;
135
+ }
@@ -0,0 +1,2 @@
1
+ import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ export declare function synchronizeShellConfiguration(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
@@ -0,0 +1,30 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export function synchronizeShellConfiguration(applicationRoot, configuration) {
4
+ const manifest = Object.fromEntries(configuration.microfrontends.map((microfrontend) => [
5
+ microfrontend.remoteName,
6
+ microfrontend.developmentRemoteEntry,
7
+ ]));
8
+ writeFileSync(join(applicationRoot, 'src', 'assets', 'federation.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
9
+ writeFileSync(join(applicationRoot, 'src', 'app', 'application.config.ts'), renderApplicationConfiguration(configuration), 'utf8');
10
+ }
11
+ function renderApplicationConfiguration(configuration) {
12
+ const entries = configuration.microfrontends
13
+ .map((microfrontend) => ` {
14
+ path: ${JSON.stringify(microfrontend.route)},
15
+ remote: ${JSON.stringify(microfrontend.remoteName)},
16
+ exposedModule: './Routes',
17
+ },`)
18
+ .join('\n');
19
+ return `export interface MicrofrontendDefinition {
20
+ readonly path: string;
21
+ readonly remote: string;
22
+ readonly exposedModule: './Routes';
23
+ }
24
+
25
+ // Este fichero lo mantiene el CLI a partir de mova.config.json.
26
+ export const microfrontends: readonly MicrofrontendDefinition[] = [
27
+ ${entries}
28
+ ];
29
+ `;
30
+ }
@@ -0,0 +1,10 @@
1
+ export declare const SHELL_REPOSITORY = "https://github.com/rgarciadelongoria/open-mova.git";
2
+ export interface ShellVersion {
3
+ readonly repository: string;
4
+ readonly version: string;
5
+ readonly commit: string;
6
+ }
7
+ export declare function listShellVersions(): string[];
8
+ export declare function downloadShell(destination: string, requestedVersion?: string): ShellVersion;
9
+ export declare function downloadTaggedProject(project: 'open-mova-shell' | 'open-mova-mf-template' | 'open-mova-core', destination: string, requestedVersion?: string): ShellVersion;
10
+ export declare function configureDownloadedShell(destination: string, applicationName: string, includesStarterMicrofrontend: boolean): void;