@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 +21 -0
- package/README.md +172 -0
- package/dist/application/configuration.d.ts +7 -0
- package/dist/application/configuration.js +135 -0
- package/dist/application/microfrontend-profile.d.ts +2 -0
- package/dist/application/microfrontend-profile.js +66 -0
- package/dist/application/microfrontend.d.ts +22 -0
- package/dist/application/microfrontend.js +135 -0
- package/dist/application/shell-configuration.d.ts +2 -0
- package/dist/application/shell-configuration.js +30 -0
- package/dist/application/shell-repository.d.ts +10 -0
- package/dist/application/shell-repository.js +141 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +31 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +34 -0
- package/dist/commands/capacitor.d.ts +2 -0
- package/dist/commands/capacitor.js +84 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +70 -0
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +26 -0
- package/dist/commands/microfrontend.d.ts +2 -0
- package/dist/commands/microfrontend.js +85 -0
- package/dist/commands/shell.d.ts +2 -0
- package/dist/commands/shell.js +18 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +69 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/dist/utils/names.d.ts +4 -0
- package/dist/utils/names.js +28 -0
- package/package.json +52 -0
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,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,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;
|