@open-mova/cli 0.1.19 → 0.1.21

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 (35) hide show
  1. package/README.md +194 -4
  2. package/dist/application/android-configuration.d.ts +2 -0
  3. package/dist/application/android-configuration.js +90 -0
  4. package/dist/application/configuration-migrations.d.ts +6 -0
  5. package/dist/application/configuration-migrations.js +57 -0
  6. package/dist/application/configuration.d.ts +5 -0
  7. package/dist/application/configuration.js +52 -19
  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/project-update.d.ts +8 -0
  20. package/dist/application/project-update.js +106 -0
  21. package/dist/application/shell-configuration.js +2 -0
  22. package/dist/application/shell-repository.d.ts +1 -1
  23. package/dist/application/shell-repository.js +14 -7
  24. package/dist/application/shell-update.d.ts +1 -5
  25. package/dist/application/shell-update.js +9 -108
  26. package/dist/cli.js +2 -0
  27. package/dist/commands/capacitor.js +15 -49
  28. package/dist/commands/create.js +2 -2
  29. package/dist/commands/doctor.d.ts +2 -0
  30. package/dist/commands/doctor.js +25 -0
  31. package/dist/commands/info.js +1 -1
  32. package/dist/commands/microfrontend.js +40 -1
  33. package/dist/commands/update.js +13 -4
  34. package/dist/types.d.ts +13 -1
  35. package/package.json +7 -3
@@ -14,12 +14,14 @@ function renderApplicationConfiguration(configuration) {
14
14
  path: ${JSON.stringify(microfrontend.route)},
15
15
  remote: ${JSON.stringify(microfrontend.remoteName)},
16
16
  exposedModule: './Routes',
17
+ requiredCoreVersion: ${JSON.stringify(microfrontend.compatibility.requiredCoreVersion)},
17
18
  },`)
18
19
  .join('\n');
19
20
  return `export interface MicrofrontendDefinition {
20
21
  readonly path: string;
21
22
  readonly remote: string;
22
23
  readonly exposedModule: './Routes';
24
+ readonly requiredCoreVersion: string;
23
25
  }
24
26
 
25
27
  // Este fichero lo mantiene el CLI a partir de mova.config.json.
@@ -1,4 +1,4 @@
1
- export declare const SHELL_REPOSITORY = "https://github.com/rgarciadelongoria/open-mova.git";
1
+ export declare const SHELL_REPOSITORY: string;
2
2
  export declare const DEMO_MICROFRONTEND_REMOTE_ENTRY = "https://rgarciadelongoria.github.io/open-mova/remoteEntry.json";
3
3
  export interface ShellVersion {
4
4
  readonly repository: string;
@@ -1,8 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
- export const SHELL_REPOSITORY = 'https://github.com/rgarciadelongoria/open-mova.git';
5
+ export const SHELL_REPOSITORY = process.env['OPEN_MOVA_SHELL_REPOSITORY'] ?? 'https://github.com/rgarciadelongoria/open-mova.git';
6
6
  // El demo publicado permite probar una aplicación recién creada sin desplegar un MF propio.
7
7
  export const DEMO_MICROFRONTEND_REMOTE_ENTRY = 'https://rgarciadelongoria.github.io/open-mova/remoteEntry.json';
8
8
  const VERSION_PATTERN = /^v(\d+)\.(\d+)\.(\d+)$/;
@@ -41,9 +41,17 @@ export function downloadTaggedProject(project, destination, requestedVersion) {
41
41
  try {
42
42
  const checkout = join(temporaryDirectory, 'repository');
43
43
  runGit([
44
- '-c', 'advice.detachedHead=false',
45
- 'clone', '--quiet', '--depth', '1', '--single-branch',
46
- '--branch', version, SHELL_REPOSITORY, checkout,
44
+ '-c',
45
+ 'advice.detachedHead=false',
46
+ 'clone',
47
+ '--quiet',
48
+ '--depth',
49
+ '1',
50
+ '--single-branch',
51
+ '--branch',
52
+ version,
53
+ SHELL_REPOSITORY,
54
+ checkout,
47
55
  ]);
48
56
  const source = join(checkout, project);
49
57
  const requiredFiles = ['package.json', 'angular.json', 'src/main.ts'];
@@ -92,8 +100,7 @@ export function configureDownloadedShell(destination, applicationName, includesS
92
100
  writeFileSync(capacitorPath, customizedConfig);
93
101
  }
94
102
  writeFileSync(join(destination, '.nvmrc'), '22\n');
95
- const installSteps = 'npm install\n' +
96
- (includesStarterMicrofrontend ? 'npm --prefix mfs/home install\n' : '');
103
+ const installSteps = 'npm install\n' + (includesStarterMicrofrontend ? 'npm --prefix mfs/home install\n' : '');
97
104
  const runSteps = includesStarterMicrofrontend
98
105
  ? 'Inicia `npm --prefix mfs/home start` y `npm start` en dos terminales. Abre `http://localhost:4200/home/inicio`.\n'
99
106
  : 'Registra primero un MF con `mova mf create nombre` y después ejecuta `mova start`.\n';
@@ -1,9 +1,6 @@
1
1
  import type { OpenMovaApplicationConfiguration } from '../types.js';
2
2
  import { type ShellVersion } from './shell-repository.js';
3
- interface FileChange {
4
- readonly path: string;
5
- readonly content?: Buffer;
6
- }
3
+ import { type FileChange } from './project-update.js';
7
4
  export interface ShellUpdatePlan {
8
5
  readonly currentVersion: string;
9
6
  readonly target: ShellVersion;
@@ -16,4 +13,3 @@ export interface ShellUpdatePlan {
16
13
  }
17
14
  export declare function createShellUpdatePlan(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, requestedVersion?: string): ShellUpdatePlan;
18
15
  export declare function applyShellUpdate(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, plan: ShellUpdatePlan): void;
19
- export {};
@@ -1,10 +1,12 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
3
- import { dirname, join, relative } from 'node:path';
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
4
3
  import { tmpdir } from 'node:os';
5
4
  import { synchronizeShellConfiguration } from './shell-configuration.js';
6
5
  import { downloadShell, listShellVersions } from './shell-repository.js';
7
6
  import { writeApplicationConfiguration } from './configuration.js';
7
+ import { applyFileChanges, compareManagedFiles, comparePackageConfiguration, requireCleanGitRepository, } from './project-update.js';
8
+ import { findIncompatibleMicrofrontends } from './core-compatibility.js';
9
+ import { readRequiredCoreVersion } from './microfrontend-manifest.js';
8
10
  const GENERATED_PATHS = new Set([
9
11
  'package.json',
10
12
  'package-lock.json',
@@ -34,8 +36,10 @@ export function createShellUpdatePlan(applicationRoot, configuration, requestedV
34
36
  throw new Error(`El commit registrado para ${configuration.shell.version} no coincide con el tag remoto.`);
35
37
  }
36
38
  const conflicts = [];
37
- const fileChanges = compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts);
39
+ const fileChanges = compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts, GENERATED_PATHS);
38
40
  const packageUpdate = comparePackageConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
41
+ const targetCoreVersion = readRequiredCoreVersion(targetDirectory);
42
+ conflicts.push(...findIncompatibleMicrofrontends(configuration, targetCoreVersion));
39
43
  const capacitorUpdate = compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
40
44
  const changes = [
41
45
  ...fileChanges.map((change) => `${change.content ? 'Actualizar' : 'Eliminar'} ${change.path}`),
@@ -65,16 +69,7 @@ export function applyShellUpdate(applicationRoot, configuration, plan) {
65
69
  throw new Error('No se puede actualizar mientras existan conflictos.');
66
70
  }
67
71
  requireCleanGitRepository(applicationRoot);
68
- for (const change of plan.fileChanges) {
69
- const destination = join(applicationRoot, change.path);
70
- if (change.content) {
71
- mkdirSync(dirname(destination), { recursive: true });
72
- writeFileSync(destination, change.content);
73
- }
74
- else {
75
- rmSync(destination, { force: true });
76
- }
77
- }
72
+ applyFileChanges(applicationRoot, plan.fileChanges);
78
73
  if (plan.packageContent) {
79
74
  writeFileSync(join(applicationRoot, 'package.json'), plan.packageContent, 'utf8');
80
75
  }
@@ -91,60 +86,6 @@ export function applyShellUpdate(applicationRoot, configuration, plan) {
91
86
  writeApplicationConfiguration(applicationRoot, updatedConfiguration);
92
87
  synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
93
88
  }
94
- function compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts) {
95
- const paths = new Set([
96
- ...listFiles(currentDirectory),
97
- ...listFiles(targetDirectory),
98
- ]);
99
- const changes = [];
100
- for (const path of [...paths].sort()) {
101
- if (GENERATED_PATHS.has(path))
102
- continue;
103
- const currentContent = readOptionalFile(join(currentDirectory, path));
104
- const targetContent = readOptionalFile(join(targetDirectory, path));
105
- const applicationContent = readOptionalFile(join(applicationRoot, path));
106
- if (buffersEqual(currentContent, targetContent))
107
- continue;
108
- if (buffersEqual(applicationContent, currentContent)) {
109
- changes.push({ path, ...(targetContent ? { content: targetContent } : {}) });
110
- continue;
111
- }
112
- if (buffersEqual(applicationContent, targetContent))
113
- continue;
114
- conflicts.push(path);
115
- }
116
- return changes;
117
- }
118
- function comparePackageConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts) {
119
- const application = readJson(join(applicationRoot, 'package.json'));
120
- const current = readJson(join(currentDirectory, 'package.json'));
121
- const target = readJson(join(targetDirectory, 'package.json'));
122
- let changed = false;
123
- for (const section of ['scripts', 'dependencies', 'devDependencies']) {
124
- const applicationSection = readStringMap(application[section]);
125
- const currentSection = readStringMap(current[section]);
126
- const targetSection = readStringMap(target[section]);
127
- const keys = new Set([...Object.keys(currentSection), ...Object.keys(targetSection)]);
128
- for (const key of keys) {
129
- if (currentSection[key] === targetSection[key])
130
- continue;
131
- if (applicationSection[key] === currentSection[key]) {
132
- if (targetSection[key] === undefined) {
133
- delete applicationSection[key];
134
- }
135
- else {
136
- applicationSection[key] = targetSection[key];
137
- }
138
- changed = true;
139
- }
140
- else if (applicationSection[key] !== targetSection[key]) {
141
- conflicts.push(`package.json#${section}.${key}`);
142
- }
143
- }
144
- application[section] = applicationSection;
145
- }
146
- return changed ? `${JSON.stringify(application, null, 2)}\n` : undefined;
147
- }
148
89
  function compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts) {
149
90
  const applicationPath = join(applicationRoot, 'capacitor.config.ts');
150
91
  const currentPath = join(currentDirectory, 'capacitor.config.ts');
@@ -163,46 +104,6 @@ function compareCapacitorConfiguration(applicationRoot, currentDirectory, target
163
104
  conflicts.push('capacitor.config.ts');
164
105
  return undefined;
165
106
  }
166
- function requireCleanGitRepository(applicationRoot) {
167
- const repository = spawnSync('git', ['-C', applicationRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
168
- if (repository.status !== 0) {
169
- throw new Error('Inicializa Git y crea un commit antes de ejecutar mova update.');
170
- }
171
- const status = spawnSync('git', ['-C', applicationRoot, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8' });
172
- if (status.status !== 0 || status.stdout.trim() !== '') {
173
- throw new Error('El repositorio debe estar limpio antes de ejecutar mova update.');
174
- }
175
- }
176
- function listFiles(directory) {
177
- const files = [];
178
- const visit = (currentDirectory) => {
179
- for (const entry of readdirSync(currentDirectory, { withFileTypes: true })) {
180
- const absolutePath = join(currentDirectory, entry.name);
181
- if (entry.isDirectory()) {
182
- visit(absolutePath);
183
- }
184
- else if (entry.isFile()) {
185
- files.push(relative(directory, absolutePath));
186
- }
187
- }
188
- };
189
- visit(directory);
190
- return files;
191
- }
192
- function readOptionalFile(path) {
193
- return existsSync(path) ? readFileSync(path) : undefined;
194
- }
195
- function buffersEqual(first, second) {
196
- return first === undefined ? second === undefined : second !== undefined && first.equals(second);
197
- }
198
- function readJson(path) {
199
- return JSON.parse(readFileSync(path, 'utf8'));
200
- }
201
- function readStringMap(value) {
202
- if (!value || typeof value !== 'object' || Array.isArray(value))
203
- return {};
204
- return { ...value };
205
- }
206
107
  function readCapacitorIdentity(content) {
207
108
  const appId = content.match(/\bappId:\s*['"]([^'"]+)['"]/)?.[1];
208
109
  const appName = content.match(/\bappName:\s*['"]([^'"]+)['"]/)?.[1];
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { Command } from 'commander';
4
4
  import { registerBuildCommand } from './commands/build.js';
5
5
  import { registerCapacitorCommands } from './commands/capacitor.js';
6
6
  import { registerCreateCommand } from './commands/create.js';
7
+ import { registerDoctorCommand } from './commands/doctor.js';
7
8
  import { registerInfoCommand } from './commands/info.js';
8
9
  import { registerMicrofrontendCommands } from './commands/microfrontend.js';
9
10
  import { registerStartCommand } from './commands/start.js';
@@ -16,6 +17,7 @@ program
16
17
  .description('Herramientas de desarrollo para aplicaciones Open Mova')
17
18
  .version(packageVersion.version);
18
19
  registerCreateCommand(program);
20
+ registerDoctorCommand(program);
19
21
  registerMicrofrontendCommands(program);
20
22
  registerStartCommand(program);
21
23
  registerShellCommands(program);
@@ -1,20 +1,23 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
5
+ import { configureAndroidProject } from '../application/android-configuration.js';
5
6
  import { localBinaryName, npmCommand, useCommandShell } from '../utils/platform.js';
6
7
  export function registerCapacitorCommands(program) {
7
8
  const capacitor = program.command('cap').description('Prepara la shell para Android o iOS');
8
- capacitor.command('add <platform>')
9
+ capacitor
10
+ .command('add <platform>')
9
11
  .description('Crea el proyecto nativo de la plataforma')
10
12
  .action((platform) => {
11
13
  const selectedPlatform = parsePlatform(platform);
12
14
  const root = requireApplicationRoot(process.cwd());
13
15
  buildMobileShell(root);
14
16
  runCapacitor(root, ['add', selectedPlatform]);
15
- configureNativePlatform(root, selectedPlatform);
17
+ configureNativePlatform(root, selectedPlatform, readApplicationConfiguration(root));
16
18
  });
17
- capacitor.command('sync [platform]')
19
+ capacitor
20
+ .command('sync [platform]')
18
21
  .description('Compila la shell y sincroniza sus assets y plugins nativos')
19
22
  .action((platform) => {
20
23
  const root = requireApplicationRoot(process.cwd());
@@ -22,13 +25,14 @@ export function registerCapacitorCommands(program) {
22
25
  const selectedPlatform = platform ? parsePlatform(platform) : undefined;
23
26
  runCapacitor(root, ['sync', ...(selectedPlatform ? [selectedPlatform] : [])]);
24
27
  if (selectedPlatform) {
25
- configureNativePlatform(root, selectedPlatform);
28
+ configureNativePlatform(root, selectedPlatform, readApplicationConfiguration(root));
26
29
  }
27
30
  else {
28
- configureExistingPlatforms(root);
31
+ configureExistingPlatforms(root, readApplicationConfiguration(root));
29
32
  }
30
33
  });
31
- capacitor.command('open <platform>')
34
+ capacitor
35
+ .command('open <platform>')
32
36
  .description('Abre el proyecto nativo en Android Studio o Xcode')
33
37
  .action((platform) => {
34
38
  const root = requireApplicationRoot(process.cwd());
@@ -72,54 +76,16 @@ function isHttpsUrl(value) {
72
76
  return false;
73
77
  }
74
78
  }
75
- function configureExistingPlatforms(root) {
79
+ function configureExistingPlatforms(root, configuration) {
76
80
  if (existsSync(join(root, 'android'))) {
77
- configureNativePlatform(root, 'android');
81
+ configureNativePlatform(root, 'android', configuration);
78
82
  }
79
83
  }
80
- function configureNativePlatform(root, platform) {
84
+ function configureNativePlatform(root, platform, configuration) {
81
85
  if (platform === 'android') {
82
- configureMinimumAndroidSdk(root);
83
- configureBackgroundRunnerForAndroid(root);
86
+ configureAndroidProject(root, configuration);
84
87
  }
85
88
  }
86
- function configureMinimumAndroidSdk(root) {
87
- const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
88
- if (!packageJson.dependencies?.['@capacitor/local-llm'])
89
- return;
90
- const variablesPath = join(root, 'android', 'variables.gradle');
91
- if (!existsSync(variablesPath)) {
92
- throw new Error('No se encuentra android/variables.gradle para configurar Local LLM.');
93
- }
94
- const variables = readFileSync(variablesPath, 'utf8');
95
- const minimumSdkPattern = /minSdkVersion\s*=\s*(\d+)/;
96
- const currentMinimumSdk = Number(minimumSdkPattern.exec(variables)?.[1]);
97
- if (!Number.isFinite(currentMinimumSdk)) {
98
- throw new Error('No se ha encontrado minSdkVersion en android/variables.gradle.');
99
- }
100
- if (currentMinimumSdk >= 28)
101
- return;
102
- writeFileSync(variablesPath, variables.replace(minimumSdkPattern, 'minSdkVersion = 28'), 'utf8');
103
- }
104
- function configureBackgroundRunnerForAndroid(root) {
105
- const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
106
- if (!packageJson.dependencies?.['@capacitor/background-runner'])
107
- return;
108
- const gradlePath = join(root, 'android', 'app', 'build.gradle');
109
- if (!existsSync(gradlePath)) {
110
- throw new Error('No se encuentra android/app/build.gradle para configurar Background Runner.');
111
- }
112
- const repositoryEntry = "dirs '../../node_modules/@capacitor/background-runner/android/src/main/libs', 'libs'";
113
- const gradle = readFileSync(gradlePath, 'utf8');
114
- if (gradle.includes(repositoryEntry))
115
- return;
116
- const flatDirectory = /flatDir\s*\{/;
117
- if (!flatDirectory.test(gradle)) {
118
- throw new Error('No se ha encontrado el bloque flatDir en android/app/build.gradle.');
119
- }
120
- const configuredGradle = gradle.replace(flatDirectory, (match) => `${match}\n ${repositoryEntry}`);
121
- writeFileSync(gradlePath, configuredGradle, 'utf8');
122
- }
123
89
  function runCapacitor(root, args) {
124
90
  if (!existsSync(join(root, 'capacitor.config.ts'))) {
125
91
  throw new Error('Esta versión de shell no incluye Capacitor. Usa una shell v0.1.2 o posterior.');
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { createMicrofrontend } from '../application/microfrontend.js';
4
- import { addMicrofrontend, writeApplicationConfiguration, } from '../application/configuration.js';
4
+ import { addMicrofrontend, writeApplicationConfiguration } from '../application/configuration.js';
5
5
  import { synchronizeShellConfiguration } from '../application/shell-configuration.js';
6
6
  import { configureDownloadedShell, DEMO_MICROFRONTEND_REMOTE_ENTRY, downloadShell, } from '../application/shell-repository.js';
7
7
  import { normalizeName } from '../utils/names.js';
@@ -27,7 +27,7 @@ export function registerCreateCommand(program) {
27
27
  shellVersion = shell.version;
28
28
  configureDownloadedShell(temporaryApplication, applicationName, !options.empty);
29
29
  let configuration = {
30
- schemaVersion: 1,
30
+ schemaVersion: 2,
31
31
  name: applicationName,
32
32
  shell,
33
33
  microfrontends: [],
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerDoctorCommand(program: Command): void;
@@ -0,0 +1,25 @@
1
+ import { inspectDevelopmentEnvironment } from '../application/doctor.js';
2
+ const STATUS_SYMBOL = {
3
+ ok: '✓',
4
+ warning: '!',
5
+ error: '✗',
6
+ };
7
+ export function registerDoctorCommand(program) {
8
+ program
9
+ .command('doctor')
10
+ .description('Comprueba el entorno y la configuración de una aplicación Open Mova')
11
+ .action(() => {
12
+ const report = inspectDevelopmentEnvironment(process.cwd());
13
+ console.log('Diagnóstico de Open Mova');
14
+ for (const check of report.checks) {
15
+ console.log(`${STATUS_SYMBOL[check.status]} ${check.message}`);
16
+ if (check.detail)
17
+ console.log(` ${check.detail}`);
18
+ }
19
+ const errors = report.checks.filter((check) => check.status === 'error').length;
20
+ const warnings = report.checks.filter((check) => check.status === 'warning').length;
21
+ console.log(`Resultado: ${errors} errores, ${warnings} avisos.`);
22
+ if (errors > 0)
23
+ process.exitCode = 1;
24
+ });
25
+ }
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { findApplicationRoot, readApplicationConfiguration, } from '../application/configuration.js';
2
+ import { findApplicationRoot, readApplicationConfiguration } from '../application/configuration.js';
3
3
  export function registerInfoCommand(program) {
4
4
  program
5
5
  .command('info')
@@ -4,6 +4,7 @@ import { addMicrofrontend, readApplicationConfiguration, requireApplicationRoot,
4
4
  import { synchronizeShellConfiguration } from '../application/shell-configuration.js';
5
5
  import { normalizeName } from '../utils/names.js';
6
6
  import { listShellVersions } from '../application/shell-repository.js';
7
+ import { applyMicrofrontendUpdate, createMicrofrontendUpdatePlan, } from '../application/microfrontend-update.js';
7
8
  export function registerMicrofrontendCommands(program) {
8
9
  const microfrontend = program
9
10
  .command('mf')
@@ -21,7 +22,9 @@ export function registerMicrofrontendCommands(program) {
21
22
  const applicationRoot = requireApplicationRoot(process.cwd());
22
23
  const configuration = readApplicationConfiguration(applicationRoot);
23
24
  const normalizedName = normalizeName(name, 'El nombre del microfrontal');
24
- const templateVersion = options.templateVersion ?? (options.demo ? configuration.shell?.version : undefined) ?? listShellVersions()[0];
25
+ const templateVersion = options.templateVersion ??
26
+ (options.demo ? configuration.shell?.version : undefined) ??
27
+ listShellVersions()[0];
25
28
  if (!templateVersion) {
26
29
  throw new Error('No hay tags estables disponibles para crear el microfrontal.');
27
30
  }
@@ -53,6 +56,7 @@ export function registerMicrofrontendCommands(program) {
53
56
  .option('--remote-entry <url>', 'URL del remoteEntry.json')
54
57
  .option('--port <number>', 'puerto de desarrollo para un proyecto local', parsePort)
55
58
  .option('--production-remote-entry <url>', 'URL HTTPS del remoto publicado')
59
+ .option('--core-version <range>', 'rango requerido de @open-mova/core')
56
60
  .action((sourcePath, options) => {
57
61
  const applicationRoot = requireApplicationRoot(process.cwd());
58
62
  const configuration = readApplicationConfiguration(applicationRoot);
@@ -64,6 +68,7 @@ export function registerMicrofrontendCommands(program) {
64
68
  remoteEntry: options.remoteEntry,
65
69
  port: options.port,
66
70
  productionRemoteEntry: options.productionRemoteEntry,
71
+ coreVersion: options.coreVersion,
67
72
  });
68
73
  const updatedConfiguration = addMicrofrontend(configuration, microfrontendConfiguration);
69
74
  writeApplicationConfiguration(applicationRoot, updatedConfiguration);
@@ -71,6 +76,40 @@ export function registerMicrofrontendCommands(program) {
71
76
  console.log(`Microfrontal "${microfrontendConfiguration.name}" registrado.`);
72
77
  console.log(`Ruta pública: /${microfrontendConfiguration.route}`);
73
78
  });
79
+ microfrontend
80
+ .command('update <name>')
81
+ .description('Actualiza un microfrontal creado desde la plantilla de Open Mova')
82
+ .option('--check', 'mostrar los cambios sin modificar archivos')
83
+ .option('--to <tag>', 'versión de destino, por ejemplo v0.2.1')
84
+ .action((name, options) => {
85
+ const applicationRoot = requireApplicationRoot(process.cwd());
86
+ const configuration = readApplicationConfiguration(applicationRoot);
87
+ const normalizedName = normalizeName(name, 'El nombre del microfrontal');
88
+ const plan = createMicrofrontendUpdatePlan(applicationRoot, configuration, normalizedName, options.to);
89
+ console.log(`Plantilla actual: ${plan.currentVersion}`);
90
+ console.log(`Plantilla destino: ${plan.target.version}`);
91
+ for (const change of plan.changes)
92
+ console.log(`- ${change}`);
93
+ if (plan.conflicts.length > 0) {
94
+ console.log('Conflictos que no se sobrescribirán:');
95
+ for (const conflict of plan.conflicts)
96
+ console.log(`- ${conflict}`);
97
+ }
98
+ if (options.check)
99
+ return;
100
+ if (plan.conflicts.length > 0) {
101
+ throw new Error('Resuelve los conflictos antes de actualizar el microfrontal.');
102
+ }
103
+ if (plan.changes.length === 0) {
104
+ console.log('El microfrontal ya está actualizado.');
105
+ return;
106
+ }
107
+ applyMicrofrontendUpdate(applicationRoot, configuration, plan);
108
+ console.log(`Microfrontal "${normalizedName}" actualizado a ${plan.target.version}.`);
109
+ if (plan.removePackageLock) {
110
+ console.log(`Ejecuta npm install en ${plan.projectRoot}.`);
111
+ }
112
+ });
74
113
  }
75
114
  function parsePort(value) {
76
115
  const port = Number(value);
@@ -1,5 +1,5 @@
1
- import { readApplicationConfiguration, requireApplicationRoot, } from '../application/configuration.js';
2
- import { applyShellUpdate, createShellUpdatePlan, } from '../application/shell-update.js';
1
+ import { readApplicationConfigurationDocument, requireApplicationRoot, } from '../application/configuration.js';
2
+ import { applyShellUpdate, createShellUpdatePlan } from '../application/shell-update.js';
3
3
  export function registerUpdateCommand(program) {
4
4
  program
5
5
  .command('update')
@@ -8,20 +8,29 @@ export function registerUpdateCommand(program) {
8
8
  .option('--to <tag>', 'versión de destino, por ejemplo v0.2.0')
9
9
  .action((options) => {
10
10
  const applicationRoot = requireApplicationRoot(process.cwd());
11
- const configuration = readApplicationConfiguration(applicationRoot);
11
+ const document = readApplicationConfigurationDocument(applicationRoot);
12
+ const configuration = document.configuration;
12
13
  const plan = createShellUpdatePlan(applicationRoot, configuration, options.to);
13
14
  printPlan(plan);
15
+ if (document.migrations.length > 0) {
16
+ console.log('Migraciones de mova.config.json:');
17
+ for (const migration of document.migrations)
18
+ console.log(`- ${migration}`);
19
+ }
14
20
  if (options.check)
15
21
  return;
16
22
  if (plan.conflicts.length > 0) {
17
23
  throw new Error('Se han detectado archivos modificados. Resuelve los conflictos antes de actualizar.');
18
24
  }
19
- if (plan.changes.length === 0) {
25
+ if (plan.changes.length === 0 && document.migrations.length === 0) {
20
26
  console.log('La aplicación ya está actualizada.');
21
27
  return;
22
28
  }
23
29
  applyShellUpdate(applicationRoot, configuration, plan);
24
30
  console.log(`Aplicación actualizada a ${plan.target.version}.`);
31
+ if (document.migrations.length > 0) {
32
+ console.log('mova.config.json se ha migrado al esquema actual.');
33
+ }
25
34
  if (plan.removePackageLock) {
26
35
  console.log('Ejecuta npm install para instalar dependencias y regenerar package-lock.json.');
27
36
  }
package/dist/types.d.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  export interface OpenMovaApplicationConfiguration {
2
- readonly schemaVersion: 1;
2
+ readonly schemaVersion: 2;
3
3
  readonly name: string;
4
4
  readonly shell?: {
5
5
  readonly repository: string;
6
6
  readonly version: string;
7
7
  readonly commit: string;
8
8
  };
9
+ /** Configuración opcional de las capacidades nativas de la aplicación. */
10
+ readonly native?: NativeConfiguration;
9
11
  readonly microfrontends: readonly MicrofrontendConfiguration[];
10
12
  }
13
+ export interface NativeConfiguration {
14
+ readonly googleMaps?: {
15
+ /** Clave de Android Maps. También puede venir de OPEN_MOVA_GOOGLE_MAPS_ANDROID_API_KEY. */
16
+ readonly androidApiKey?: string;
17
+ };
18
+ }
11
19
  export interface MicrofrontendConfiguration {
12
20
  readonly name: string;
13
21
  readonly route: string;
@@ -16,6 +24,10 @@ export interface MicrofrontendConfiguration {
16
24
  readonly developmentRemoteEntry: string;
17
25
  readonly productionRemoteEntry?: string;
18
26
  readonly sourcePath?: string;
27
+ readonly compatibility: {
28
+ /** Rango de @open-mova/core declarado y publicado por el microfrontal. */
29
+ readonly requiredCoreVersion: string;
30
+ };
19
31
  readonly template?: {
20
32
  readonly repository: string;
21
33
  readonly version: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mova/cli",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "CLI para crear y mantener aplicaciones Open Mova",
5
5
  "license": "MIT",
6
6
  "author": "Open Mova contributors",
@@ -39,13 +39,17 @@
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "start": "tsx src/cli.ts",
42
- "typecheck": "tsc --noEmit"
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "npm run build && npm run test:unit",
44
+ "test:unit": "node --test test/*.test.mjs"
43
45
  },
44
46
  "dependencies": {
45
- "commander": "^14.0.0"
47
+ "commander": "^14.0.0",
48
+ "semver": "^7.8.5"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@types/node": "^24.0.0",
52
+ "@types/semver": "^7.8.0",
49
53
  "tsx": "^4.20.0",
50
54
  "typescript": "^5.9.3"
51
55
  }