@open-mova/cli 0.1.20 → 0.1.22

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.
Files changed (38) hide show
  1. package/README.md +237 -6
  2. package/dist/application/android-configuration.d.ts +2 -0
  3. package/dist/application/android-configuration.js +115 -0
  4. package/dist/application/configuration-migrations.d.ts +6 -0
  5. package/dist/application/configuration-migrations.js +103 -0
  6. package/dist/application/configuration.d.ts +5 -0
  7. package/dist/application/configuration.js +38 -20
  8. package/dist/application/core-compatibility.d.ts +3 -0
  9. package/dist/application/core-compatibility.js +10 -0
  10. package/dist/application/doctor.d.ts +12 -0
  11. package/dist/application/doctor.js +304 -0
  12. package/dist/application/microfrontend-manifest.d.ts +3 -0
  13. package/dist/application/microfrontend-manifest.js +20 -0
  14. package/dist/application/microfrontend-profile.js +2 -1
  15. package/dist/application/microfrontend-update.d.ts +17 -0
  16. package/dist/application/microfrontend-update.js +117 -0
  17. package/dist/application/microfrontend.d.ts +1 -0
  18. package/dist/application/microfrontend.js +14 -3
  19. package/dist/application/native-capabilities.d.ts +15 -0
  20. package/dist/application/native-capabilities.js +143 -0
  21. package/dist/application/project-update.d.ts +8 -0
  22. package/dist/application/project-update.js +106 -0
  23. package/dist/application/shell-configuration.d.ts +20 -0
  24. package/dist/application/shell-configuration.js +52 -1
  25. package/dist/application/shell-repository.d.ts +1 -1
  26. package/dist/application/shell-repository.js +14 -7
  27. package/dist/application/shell-update.d.ts +1 -5
  28. package/dist/application/shell-update.js +11 -108
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/capacitor.js +55 -90
  31. package/dist/commands/create.js +3 -2
  32. package/dist/commands/doctor.d.ts +2 -0
  33. package/dist/commands/doctor.js +25 -0
  34. package/dist/commands/info.js +1 -1
  35. package/dist/commands/microfrontend.js +40 -1
  36. package/dist/commands/update.js +13 -4
  37. package/dist/types.d.ts +7 -1
  38. package/package.json +7 -3
package/README.md CHANGED
@@ -10,14 +10,23 @@ El CLI descarga la shell, el core y la plantilla de microfrontales desde tags es
10
10
 
11
11
  ## Índice de comandos
12
12
 
13
+ - [`mova --help` y `mova --version`](#mova---help-y-mova---version)
13
14
  - [`mova create`](#mova-create)
15
+ - [`mova mf`](#mova-mf)
14
16
  - [`mova mf create`](#mova-mf-create)
15
17
  - [`mova mf add`](#mova-mf-add)
16
18
  - [`mova start`](#mova-start)
17
19
  - [`mova build`](#mova-build)
18
20
  - [`mova info`](#mova-info)
21
+ - [`mova doctor`](#mova-doctor)
19
22
  - [`mova update`](#mova-update)
23
+ - [`mova shell`](#mova-shell)
20
24
  - [`mova shell versions`](#mova-shell-versions)
25
+ - [`mova cap`](#mova-cap)
26
+ - [`mova cap list`](#mova-cap-list)
27
+ - [`mova cap enable`](#mova-cap-enable)
28
+ - [`mova cap disable`](#mova-cap-disable)
29
+ - [`mova cap doctor`](#mova-cap-doctor)
21
30
  - [`mova cap add`](#mova-cap-add)
22
31
  - [`mova cap sync`](#mova-cap-sync)
23
32
  - [`mova cap open`](#mova-cap-open)
@@ -47,6 +56,17 @@ npm link
47
56
 
48
57
  ## Comandos
49
58
 
59
+ ### `mova --help` y `mova --version`
60
+
61
+ Muestra la ayuda del CLI o la versión instalada.
62
+
63
+ ```bash
64
+ mova --help
65
+ mova --version
66
+ mova mf --help
67
+ mova cap --help
68
+ ```
69
+
50
70
  ### `mova create`
51
71
 
52
72
  Crea una aplicación completa. Descarga una shell versionada, el core y un MF
@@ -63,6 +83,11 @@ mova create mi-aplicacion --empty
63
83
 
64
84
  Sin `--shell-version` se usa el tag estable más reciente (`vMAJOR.MINOR.PATCH`). `--empty` omite el MF inicial.
65
85
 
86
+ ### `mova mf`
87
+
88
+ Es el grupo de comandos para crear y registrar microfrontales en la aplicación.
89
+ Usa `mova mf --help` para consultar sus subcomandos.
90
+
66
91
  ### `mova mf create`
67
92
 
68
93
  Crea un microfrontal local, lo registra y actualiza la configuración de federación de la shell.
@@ -97,6 +122,32 @@ mova mf add --name catalog --route productos \
97
122
 
98
123
  El CLI actualiza `mova.config.json`, `src/assets/federation.manifest.json` y `src/app/application.config.ts`.
99
124
 
125
+ Para un MF remoto que no tenga un proyecto local, declara explícitamente su
126
+ rango de Core:
127
+
128
+ ```bash
129
+ mova mf add --name catalog --route catalog \
130
+ --remote catalog-microfrontend \
131
+ --remote-entry https://cdn.example.com/catalog/remoteEntry.json \
132
+ --core-version '^0.2.2'
133
+ ```
134
+
135
+ ### `mova mf update`
136
+
137
+ Actualiza un MF local que fue creado desde una plantilla de Open Mova. Compara
138
+ la plantilla original, el proyecto actual y la nueva versión en tres pasos:
139
+ solo aplica cambios que no hayan sido personalizados y deja los conflictos
140
+ para revisión.
141
+
142
+ ```bash
143
+ mova mf update home --check
144
+ mova mf update home --to v0.2.2
145
+ ```
146
+
147
+ El comando conserva el nombre, la ruta, el puerto, el perfil y las
148
+ dependencias propias del proveedor. Si cambia `package.json`, elimina el
149
+ lockfile para que ejecutes `npm install` en el MF.
150
+
100
151
  ### `mova start`
101
152
 
102
153
  Arranca los MFs locales registrados y después la shell. Los MFs configurados solo con una URL remota no se arrancan localmente.
@@ -122,6 +173,25 @@ Muestra la aplicación detectada, la versión de shell y sus microfrontales. Tam
122
173
  mova info
123
174
  ```
124
175
 
176
+ ### `mova doctor`
177
+
178
+ Diagnostica el entorno de desarrollo y la aplicación encontrada desde el
179
+ directorio actual. Comprueba las herramientas básicas, las dependencias, la
180
+ configuración de la shell y los microfrontales, las URLs de producción y la
181
+ compatibilidad de Core.
182
+
183
+ Si la aplicación contiene plataformas Capacitor, también revisa los
184
+ requisitos disponibles de Android o iOS, como el SDK, `adb`, emuladores,
185
+ Xcode, CocoaPods, claves y descripciones de permisos.
186
+
187
+ ```bash
188
+ mova doctor
189
+ ```
190
+
191
+ Los avisos informan de elementos opcionales o todavía no configurados. Los
192
+ errores hacen que el comando termine con un código distinto de cero, por lo
193
+ que también puede utilizarse en scripts de validación.
194
+
125
195
  ### `mova update`
126
196
 
127
197
  Actualiza los archivos técnicos de la shell usando como referencia el tag con
@@ -147,15 +217,63 @@ Lista los tags estables disponibles para crear aplicaciones.
147
217
  mova shell versions
148
218
  ```
149
219
 
220
+ ### `mova shell`
221
+
222
+ Es el grupo de comandos relacionado con las versiones de la shell. Actualmente
223
+ su subcomando disponible es `mova shell versions`.
224
+
150
225
  ### `mova cap add`
151
226
 
152
- Compila la aplicación y añade una plataforma nativa. Configura antes el `appId` definitivo en `capacitor.config.ts`.
227
+ Compila la shell, añade una plataforma nativa y aplica su configuración inicial.
228
+ El `appId` y el `appName` se generan al crear la aplicación; revísalos en
229
+ `capacitor.config.ts` si necesitas personalizarlos antes de distribuirla.
153
230
 
154
231
  ```bash
155
232
  mova cap add android
156
233
  mova cap add ios
157
234
  ```
158
235
 
236
+ ### `mova cap list`
237
+
238
+ Muestra el catálogo de capacidades nativas. `✓` indica que la capacidad está
239
+ habilitada y `○` que está disponible pero no instalada.
240
+
241
+ ```bash
242
+ mova cap list
243
+ ```
244
+
245
+ ### `mova cap enable`
246
+
247
+ Habilita una o varias capacidades. Instala únicamente sus paquetes npm,
248
+ actualiza `mova.config.json`, regenera el provider Angular y sincroniza las
249
+ plataformas nativas que ya existan.
250
+
251
+ ```bash
252
+ mova cap enable camera device geolocation
253
+ ```
254
+
255
+ ### `mova cap disable`
256
+
257
+ Deshabilita capacidades y elimina los paquetes que ninguna otra capacidad
258
+ activa necesita. El contrato de Core permanece disponible, pero
259
+ `isAvailable()` devuelve `false` y cualquier operación explica cómo volver a
260
+ habilitar la capacidad.
261
+
262
+ ```bash
263
+ mova cap disable geolocation
264
+ ```
265
+
266
+ ### `mova cap doctor`
267
+
268
+ Muestra plataformas compatibles, permisos, SDK mínimo y configuración manual
269
+ de las capacidades activas.
270
+
271
+ ```bash
272
+ mova cap doctor
273
+ mova cap doctor android
274
+ mova cap doctor ios
275
+ ```
276
+
159
277
  ### `mova cap sync`
160
278
 
161
279
  Sincroniza la shell compilada, los recursos y los plugins con la plataforma. Sin plataforma, sincroniza todas las plataformas añadidas.
@@ -165,9 +283,8 @@ mova cap sync android
165
283
  mova cap sync
166
284
  ```
167
285
 
168
- En Android, el CLI aplica también los requisitos técnicos de los plugins
169
- incluidos por la shell: configura el repositorio AAR de Background Runner y
170
- eleva `minSdkVersion` a 28 cuando está instalado Local LLM. También añade la
286
+ En Android, el CLI aplica los requisitos declarados por las capacidades
287
+ activas: permisos, SDK mínimo, el repositorio AAR de Background Runner y la
171
288
  entrada obligatoria de Google Maps. Para usar mapas reales, define una clave
172
289
  restringida antes de sincronizar:
173
290
 
@@ -198,6 +315,12 @@ mova cap open android
198
315
  mova cap open ios
199
316
  ```
200
317
 
318
+ ### `mova cap`
319
+
320
+ Es el grupo de comandos para preparar y abrir los proyectos nativos de
321
+ Capacitor. También permite seleccionar capacidades mediante `list`, `enable`,
322
+ `disable` y `doctor`.
323
+
201
324
  ## Flujo habitual
202
325
 
203
326
  ```bash
@@ -205,11 +328,119 @@ mova create mi-aplicacion
205
328
  cd mi-aplicacion
206
329
  npm install
207
330
  npm --prefix mfs/home install
331
+ mova cap enable camera device
208
332
  mova start
209
333
  ```
210
334
 
211
335
  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.
212
336
 
213
- ## Limitaciones actuales
337
+ ## Flujo para Android
338
+
339
+ > [!WARNING]
340
+ > Antes de empezar debes tener instalado Android Studio, el Android SDK, las
341
+ > Build Tools, Platform Tools (`adb`) y un emulador o un dispositivo Android.
342
+ > También necesitas Node.js 22, npm y Git. Android Studio incluye un JDK
343
+ > compatible, por lo que normalmente no necesitas instalar otro manualmente.
344
+
345
+ Desde el directorio donde quieras crear la aplicación:
346
+
347
+ ```bash
348
+ mova create mi-aplicacion
349
+ cd mi-aplicacion
350
+ npm install
351
+ npm --prefix mfs/home install
352
+ ```
353
+
354
+ Comprueba que cada microfrontal tiene un `productionRemoteEntry` HTTPS en
355
+ `mova.config.json`. Android no puede utilizar las URLs de desarrollo de
356
+ `localhost` como si fuera el navegador del ordenador.
357
+
358
+ Añade la plataforma Android. El comando compila la shell, ejecuta Capacitor y
359
+ aplica la configuración específica de los plugins instalados:
360
+
361
+ ```bash
362
+ mova cap add android
363
+ ```
364
+
365
+ Antes o después de añadir la plataforma puedes habilitar solo lo que use la
366
+ aplicación:
367
+
368
+ ```bash
369
+ mova cap enable camera device
370
+ mova cap doctor android
371
+ ```
372
+
373
+ Si utilizas Google Maps, proporciona la clave antes de sincronizar:
374
+
375
+ ```bash
376
+ OPEN_MOVA_GOOGLE_MAPS_ANDROID_API_KEY=tu_clave mova cap sync android
377
+ ```
378
+
379
+ También puedes guardarla en `mova.config.json`, como se explica en la sección
380
+ [`mova cap sync`](#mova-cap-sync). Para volver a sincronizar después de
381
+ cambiar la shell, los plugins o la configuración:
382
+
383
+ ```bash
384
+ mova cap sync android
385
+ ```
386
+
387
+ Abre el proyecto nativo en Android Studio:
388
+
389
+ ```bash
390
+ mova cap open android
391
+ ```
392
+
393
+ En Android Studio selecciona un emulador o dispositivo, espera a que termine
394
+ la sincronización de Gradle y pulsa **Run**. Para probar una nueva versión del
395
+ MF remoto no hace falta recompilar Android si la URL publicada no cambia; la
396
+ shell lo cargará al iniciar la aplicación.
397
+
398
+ ## Flujo para iOS
399
+
400
+ > [!WARNING]
401
+ > Antes de empezar debes tener instalado Xcode, sus Command Line Tools y
402
+ > CocoaPods. Necesitas macOS, Node.js 22, npm y Git. Para ejecutar la app en un
403
+ > dispositivo físico necesitarás también una cuenta de Apple y la firma
404
+ > configurada en Xcode; el simulador no requiere un dispositivo registrado.
405
+
406
+ Desde el directorio donde quieras crear la aplicación:
407
+
408
+ ```bash
409
+ mova create mi-aplicacion
410
+ cd mi-aplicacion
411
+ npm install
412
+ npm --prefix mfs/home install
413
+ ```
414
+
415
+ Comprueba que cada microfrontal tiene un `productionRemoteEntry` HTTPS en
416
+ `mova.config.json`. La aplicación iOS seguirá cargando esos microfrontales
417
+ remotos desde la shell; no se copian dentro del proyecto nativo.
418
+
419
+ Añade y sincroniza la plataforma iOS:
420
+
421
+ ```bash
422
+ mova cap add ios
423
+ mova cap sync ios
424
+ ```
425
+
426
+ Activa previamente las capacidades que necesites y revisa sus requisitos:
427
+
428
+ ```bash
429
+ mova cap enable camera device
430
+ mova cap doctor ios
431
+ ```
432
+
433
+ Revisa en Xcode los permisos y la configuración nativa que necesiten los
434
+ plugins utilizados, por ejemplo las descripciones de cámara, fotos,
435
+ geolocalización o notificaciones en `Info.plist` y los entitlements
436
+ correspondientes.
437
+
438
+ Abre el proyecto en Xcode:
439
+
440
+ ```bash
441
+ mova cap open ios
442
+ ```
214
443
 
215
- 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.
444
+ En Xcode selecciona un simulador o dispositivo, elige el esquema de la app y
445
+ pulsa **Run**. Después de cambiar la shell o los plugins, ejecuta de nuevo
446
+ `mova cap sync ios` antes de volver a abrir o ejecutar el proyecto.
@@ -0,0 +1,2 @@
1
+ import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ export declare function configureAndroidProject(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
@@ -0,0 +1,115 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { readNativeCapabilityCatalog } from './shell-configuration.js';
4
+ export function configureAndroidProject(applicationRoot, configuration) {
5
+ const packageConfiguration = readPackageConfiguration(applicationRoot);
6
+ configureMinimumSdk(applicationRoot, configuration);
7
+ configurePermissions(applicationRoot, configuration);
8
+ configureBackgroundRunner(applicationRoot, packageConfiguration);
9
+ configureGoogleMaps(applicationRoot, configuration, packageConfiguration);
10
+ }
11
+ function configureGoogleMaps(applicationRoot, configuration, packageConfiguration) {
12
+ if (!packageConfiguration.dependencies?.['@capacitor/google-maps'])
13
+ return;
14
+ const manifestPath = join(applicationRoot, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
15
+ if (!existsSync(manifestPath)) {
16
+ throw new Error('No se encuentra AndroidManifest.xml para configurar Google Maps.');
17
+ }
18
+ const manifest = readFileSync(manifestPath, 'utf8');
19
+ const apiKeyMetadata = 'android:name="com.google.android.geo.API_KEY"';
20
+ if (!manifest.includes(apiKeyMetadata)) {
21
+ const applicationCloseTag = '</application>';
22
+ if (!manifest.includes(applicationCloseTag)) {
23
+ throw new Error('No se ha encontrado el elemento application en AndroidManifest.xml.');
24
+ }
25
+ const metadata = [
26
+ ' <meta-data',
27
+ ` ${apiKeyMetadata}`,
28
+ ' android:value="@string/open_mova_google_maps_api_key" />',
29
+ ].join('\n');
30
+ writeFileSync(manifestPath, manifest.replace(applicationCloseTag, `${metadata}\n ${applicationCloseTag}`), 'utf8');
31
+ }
32
+ const valuesDirectory = join(applicationRoot, 'android', 'app', 'src', 'main', 'res', 'values');
33
+ mkdirSync(valuesDirectory, { recursive: true });
34
+ const apiKey = process.env['OPEN_MOVA_GOOGLE_MAPS_ANDROID_API_KEY'] ??
35
+ configuration.native?.googleMaps?.androidApiKey ??
36
+ // Google Maps aborta la aplicación si falta por completo esta entrada.
37
+ 'OPEN_MOVA_GOOGLE_MAPS_API_KEY_NOT_CONFIGURED';
38
+ const resource = [
39
+ '<?xml version="1.0" encoding="utf-8"?>',
40
+ '<resources>',
41
+ ` <string name="open_mova_google_maps_api_key" translatable="false">${escapeXml(apiKey)}</string>`,
42
+ '</resources>',
43
+ '',
44
+ ].join('\n');
45
+ writeFileSync(join(valuesDirectory, 'open_mova_google_maps.xml'), resource, 'utf8');
46
+ }
47
+ function configureMinimumSdk(applicationRoot, configuration) {
48
+ const enabled = new Set(configuration.native?.capabilities ?? []);
49
+ const requiredMinimumSdk = Math.max(0, ...readNativeCapabilityCatalog(applicationRoot)
50
+ .capabilities.filter((capability) => enabled.has(capability.name))
51
+ .map((capability) => capability.minimumAndroidSdk ?? 0));
52
+ if (requiredMinimumSdk === 0)
53
+ return;
54
+ const variablesPath = join(applicationRoot, 'android', 'variables.gradle');
55
+ if (!existsSync(variablesPath)) {
56
+ throw new Error('No se encuentra android/variables.gradle para configurar Local LLM.');
57
+ }
58
+ const variables = readFileSync(variablesPath, 'utf8');
59
+ const minimumSdkPattern = /minSdkVersion\s*=\s*(\d+)/;
60
+ const currentMinimumSdk = Number(minimumSdkPattern.exec(variables)?.[1]);
61
+ if (!Number.isFinite(currentMinimumSdk)) {
62
+ throw new Error('No se ha encontrado minSdkVersion en android/variables.gradle.');
63
+ }
64
+ if (currentMinimumSdk >= requiredMinimumSdk)
65
+ return;
66
+ writeFileSync(variablesPath, variables.replace(minimumSdkPattern, `minSdkVersion = ${requiredMinimumSdk}`), 'utf8');
67
+ }
68
+ function configurePermissions(applicationRoot, configuration) {
69
+ const manifestPath = join(applicationRoot, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
70
+ if (!existsSync(manifestPath))
71
+ return;
72
+ const enabled = new Set(configuration.native?.capabilities ?? []);
73
+ const permissions = [
74
+ ...new Set(readNativeCapabilityCatalog(applicationRoot)
75
+ .capabilities.filter((capability) => enabled.has(capability.name))
76
+ .flatMap((capability) => capability.permissions?.android ?? [])),
77
+ ];
78
+ let manifest = readFileSync(manifestPath, 'utf8');
79
+ const applicationTag = /\s*<application\b/;
80
+ for (const permission of permissions) {
81
+ if (manifest.includes(`android:name="${permission}"`))
82
+ continue;
83
+ manifest = manifest.replace(applicationTag, `\n <uses-permission android:name="${permission}" />\n\n <application`);
84
+ }
85
+ writeFileSync(manifestPath, manifest, 'utf8');
86
+ }
87
+ function configureBackgroundRunner(applicationRoot, packageConfiguration) {
88
+ if (!packageConfiguration.dependencies?.['@capacitor/background-runner'])
89
+ return;
90
+ const gradlePath = join(applicationRoot, 'android', 'app', 'build.gradle');
91
+ if (!existsSync(gradlePath)) {
92
+ throw new Error('No se encuentra android/app/build.gradle para configurar Background Runner.');
93
+ }
94
+ const repositoryEntry = "dirs '../../node_modules/@capacitor/background-runner/android/src/main/libs', 'libs'";
95
+ const gradle = readFileSync(gradlePath, 'utf8');
96
+ if (gradle.includes(repositoryEntry))
97
+ return;
98
+ const flatDirectory = /flatDir\s*\{/;
99
+ if (!flatDirectory.test(gradle)) {
100
+ throw new Error('No se ha encontrado el bloque flatDir en android/app/build.gradle.');
101
+ }
102
+ writeFileSync(gradlePath, gradle.replace(flatDirectory, (match) => `${match}\n ${repositoryEntry}`), 'utf8');
103
+ }
104
+ function readPackageConfiguration(applicationRoot) {
105
+ return JSON.parse(readFileSync(join(applicationRoot, 'package.json'), 'utf8'));
106
+ }
107
+ function escapeXml(value) {
108
+ return value.replace(/[&<>"']/g, (character) => ({
109
+ '&': '&amp;',
110
+ '<': '&lt;',
111
+ '>': '&gt;',
112
+ '"': '&quot;',
113
+ "'": '&apos;',
114
+ })[character] ?? character);
115
+ }
@@ -0,0 +1,6 @@
1
+ export declare const LATEST_CONFIGURATION_SCHEMA_VERSION = 3;
2
+ export interface ConfigurationMigrationResult {
3
+ readonly value: unknown;
4
+ readonly migrations: readonly string[];
5
+ }
6
+ export declare function migrateConfiguration(value: unknown, applicationRoot: string): ConfigurationMigrationResult;
@@ -0,0 +1,103 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ export const LATEST_CONFIGURATION_SCHEMA_VERSION = 3;
4
+ export function migrateConfiguration(value, applicationRoot) {
5
+ if (!isRecord(value) || typeof value.schemaVersion !== 'number') {
6
+ return { value, migrations: [] };
7
+ }
8
+ if (value.schemaVersion > LATEST_CONFIGURATION_SCHEMA_VERSION) {
9
+ throw new Error(`mova.config.json usa schemaVersion ${value.schemaVersion}, pero este CLI solo admite hasta ${LATEST_CONFIGURATION_SCHEMA_VERSION}.`);
10
+ }
11
+ let migrated = value;
12
+ const migrations = [];
13
+ if (migrated.schemaVersion === 1) {
14
+ migrated = migrateVersionOne(migrated, applicationRoot);
15
+ migrations.push('1 → 2: declarar la compatibilidad de Core de cada microfrontal');
16
+ }
17
+ if (migrated.schemaVersion === 2) {
18
+ migrated = migrateVersionTwo(migrated, applicationRoot);
19
+ migrations.push('2 → 3: declarar las capacidades nativas habilitadas');
20
+ }
21
+ return { value: migrated, migrations };
22
+ }
23
+ function migrateVersionTwo(configuration, applicationRoot) {
24
+ const currentNative = isRecord(configuration.native) ? configuration.native : {};
25
+ return {
26
+ ...configuration,
27
+ schemaVersion: 3,
28
+ native: {
29
+ ...currentNative,
30
+ // Las aplicaciones anteriores incluían todos los plugins. Se conservan
31
+ // explícitamente hasta que el equipo decida deshabilitarlos.
32
+ capabilities: readLegacyCapabilities(configuration, applicationRoot),
33
+ },
34
+ };
35
+ }
36
+ function readLegacyCapabilities(configuration, applicationRoot) {
37
+ if (isRecord(configuration.native) && Array.isArray(configuration.native.capabilities)) {
38
+ return configuration.native.capabilities.filter((capability) => typeof capability === 'string');
39
+ }
40
+ const packageConfiguration = readJson(join(applicationRoot, 'package.json'));
41
+ const catalog = readJson(join(applicationRoot, 'native-capabilities.catalog.json'));
42
+ const dependencies = isRecord(packageConfiguration?.dependencies)
43
+ ? packageConfiguration.dependencies
44
+ : {};
45
+ if (!Array.isArray(catalog?.capabilities))
46
+ return [];
47
+ return catalog.capabilities
48
+ .filter((capability) => isRecord(capability) &&
49
+ typeof capability.name === 'string' &&
50
+ typeof capability.package === 'string')
51
+ .filter((capability) => capability.package === '@capacitor/core' || capability.package in dependencies)
52
+ .map((capability) => capability.name);
53
+ }
54
+ function readJson(path) {
55
+ if (!existsSync(path))
56
+ return undefined;
57
+ try {
58
+ const value = JSON.parse(readFileSync(path, 'utf8'));
59
+ return isRecord(value) ? value : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ function migrateVersionOne(configuration, applicationRoot) {
66
+ const shellCoreVersion = readCoreVersion(join(applicationRoot, 'package.json'));
67
+ const microfrontends = Array.isArray(configuration.microfrontends)
68
+ ? configuration.microfrontends.map((entry) => {
69
+ if (!isRecord(entry))
70
+ return entry;
71
+ const sourcePath = typeof entry.sourcePath === 'string' ? entry.sourcePath : undefined;
72
+ const localCoreVersion = sourcePath
73
+ ? readCoreVersion(join(resolve(applicationRoot, sourcePath), 'package.json'))
74
+ : undefined;
75
+ const requiredCoreVersion = localCoreVersion ?? shellCoreVersion;
76
+ return {
77
+ ...entry,
78
+ // El comodín conserva aplicaciones antiguas cuyo remoto no tenía
79
+ // metadatos; se debe concretar al actualizar el MF.
80
+ compatibility: { requiredCoreVersion: requiredCoreVersion ?? '*' },
81
+ };
82
+ })
83
+ : configuration.microfrontends;
84
+ return {
85
+ ...configuration,
86
+ schemaVersion: 2,
87
+ microfrontends,
88
+ };
89
+ }
90
+ function readCoreVersion(packagePath) {
91
+ if (!existsSync(packagePath))
92
+ return undefined;
93
+ try {
94
+ const packageConfiguration = JSON.parse(readFileSync(packagePath, 'utf8'));
95
+ return packageConfiguration.dependencies?.['@open-mova/core'];
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ function isRecord(value) {
102
+ return typeof value === 'object' && value !== null;
103
+ }
@@ -1,7 +1,12 @@
1
1
  import type { MicrofrontendConfiguration, OpenMovaApplicationConfiguration } from '../types.js';
2
2
  export declare const APPLICATION_CONFIGURATION_FILE = "mova.config.json";
3
+ export interface ApplicationConfigurationDocument {
4
+ readonly configuration: OpenMovaApplicationConfiguration;
5
+ readonly migrations: readonly string[];
6
+ }
3
7
  export declare function findApplicationRoot(startDirectory: string): string | undefined;
4
8
  export declare function requireApplicationRoot(startDirectory: string): string;
5
9
  export declare function readApplicationConfiguration(applicationRoot: string): OpenMovaApplicationConfiguration;
10
+ export declare function readApplicationConfigurationDocument(applicationRoot: string): ApplicationConfigurationDocument;
6
11
  export declare function writeApplicationConfiguration(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
7
12
  export declare function addMicrofrontend(configuration: OpenMovaApplicationConfiguration, microfrontend: MicrofrontendConfiguration): OpenMovaApplicationConfiguration;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
+ import { LATEST_CONFIGURATION_SCHEMA_VERSION, migrateConfiguration, } from './configuration-migrations.js';
3
4
  export const APPLICATION_CONFIGURATION_FILE = 'mova.config.json';
4
5
  export function findApplicationRoot(startDirectory) {
5
6
  let currentDirectory = resolve(startDirectory);
@@ -22,17 +23,16 @@ export function requireApplicationRoot(startDirectory) {
22
23
  return applicationRoot;
23
24
  }
24
25
  export function readApplicationConfiguration(applicationRoot) {
26
+ return readApplicationConfigurationDocument(applicationRoot).configuration;
27
+ }
28
+ export function readApplicationConfigurationDocument(applicationRoot) {
25
29
  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
- }
30
+ const parsed = JSON.parse(readFileSync(configurationPath, 'utf8'));
31
+ const migration = migrateConfiguration(parsed, applicationRoot);
32
+ return {
33
+ configuration: validateApplicationConfiguration(migration.value, configurationPath),
34
+ migrations: migration.migrations,
35
+ };
36
36
  }
37
37
  export function writeApplicationConfiguration(applicationRoot, configuration) {
38
38
  const configurationPath = join(applicationRoot, APPLICATION_CONFIGURATION_FILE);
@@ -57,7 +57,8 @@ function validateApplicationConfiguration(value, configurationPath) {
57
57
  if (!isRecord(value)) {
58
58
  throw new Error(`${configurationPath} no contiene un objeto JSON válido.`);
59
59
  }
60
- if (value.schemaVersion !== 1 || typeof value.name !== 'string') {
60
+ if (value.schemaVersion !== LATEST_CONFIGURATION_SCHEMA_VERSION ||
61
+ typeof value.name !== 'string') {
61
62
  throw new Error(`${configurationPath} no tiene el formato de Open Mova esperado.`);
62
63
  }
63
64
  if (!Array.isArray(value.microfrontends)) {
@@ -82,24 +83,30 @@ function validateApplicationConfiguration(value, configurationPath) {
82
83
  if (!isRecord(value.native)) {
83
84
  throw new Error(`${configurationPath} contiene una configuración nativa no válida.`);
84
85
  }
86
+ if (!Array.isArray(value.native.capabilities) ||
87
+ value.native.capabilities.some((capability) => typeof capability !== 'string')) {
88
+ throw new Error(`${configurationPath} contiene capacidades nativas no válidas.`);
89
+ }
90
+ const capabilities = [...new Set(value.native.capabilities)].sort();
85
91
  if (value.native.googleMaps !== undefined) {
86
92
  if (!isRecord(value.native.googleMaps) ||
87
93
  typeof value.native.googleMaps.androidApiKey !== 'string') {
88
94
  throw new Error(`${configurationPath} contiene una clave de Google Maps no válida.`);
89
95
  }
90
96
  native = {
97
+ capabilities,
91
98
  googleMaps: {
92
99
  androidApiKey: value.native.googleMaps.androidApiKey,
93
100
  },
94
101
  };
95
102
  }
96
103
  else {
97
- native = {};
104
+ native = { capabilities };
98
105
  }
99
106
  }
100
107
  const microfrontends = value.microfrontends.map((entry) => validateMicrofrontend(entry, configurationPath));
101
108
  return {
102
- schemaVersion: 1,
109
+ schemaVersion: LATEST_CONFIGURATION_SCHEMA_VERSION,
103
110
  name: value.name,
104
111
  ...(shell ? { shell } : {}),
105
112
  ...(native ? { native } : {}),
@@ -130,12 +137,18 @@ function validateMicrofrontend(value, configurationPath) {
130
137
  typeof value.productionRemoteEntry !== 'string') {
131
138
  throw new Error(`${configurationPath} contiene una URL de producción no válida.`);
132
139
  }
133
- if (value.template !== undefined && (!isRecord(value.template) ||
134
- typeof value.template.repository !== 'string' ||
135
- typeof value.template.version !== 'string' ||
136
- typeof value.template.commit !== 'string' ||
137
- value.template.project !== 'open-mova-mf-template' ||
138
- (value.template.profile !== 'minimal' && value.template.profile !== 'demo'))) {
140
+ if (!isRecord(value.compatibility) ||
141
+ typeof value.compatibility.requiredCoreVersion !== 'string' ||
142
+ value.compatibility.requiredCoreVersion.trim() === '') {
143
+ throw new Error(`${configurationPath} contiene un microfrontal sin compatibilidad de Core declarada.`);
144
+ }
145
+ if (value.template !== undefined &&
146
+ (!isRecord(value.template) ||
147
+ typeof value.template.repository !== 'string' ||
148
+ typeof value.template.version !== 'string' ||
149
+ typeof value.template.commit !== 'string' ||
150
+ value.template.project !== 'open-mova-mf-template' ||
151
+ (value.template.profile !== 'minimal' && value.template.profile !== 'demo'))) {
139
152
  throw new Error(`${configurationPath} contiene una plantilla de microfrontal no válida.`);
140
153
  }
141
154
  return {
@@ -148,7 +161,12 @@ function validateMicrofrontend(value, configurationPath) {
148
161
  ? {}
149
162
  : { productionRemoteEntry: value.productionRemoteEntry }),
150
163
  ...(value.sourcePath === undefined ? {} : { sourcePath: value.sourcePath }),
151
- ...(value.template === undefined ? {} : { template: value.template }),
164
+ compatibility: {
165
+ requiredCoreVersion: value.compatibility.requiredCoreVersion,
166
+ },
167
+ ...(value.template === undefined
168
+ ? {}
169
+ : { template: value.template }),
152
170
  };
153
171
  }
154
172
  function isRecord(value) {