@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
@@ -0,0 +1,304 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { findApplicationRoot, readApplicationConfigurationDocument } from './configuration.js';
5
+ import { npmCommand } from '../utils/platform.js';
6
+ import { areCoreRangesCompatible } from './core-compatibility.js';
7
+ export function inspectDevelopmentEnvironment(startDirectory) {
8
+ const checks = [];
9
+ const applicationRoot = findApplicationRoot(resolve(startDirectory));
10
+ checks.push(checkNodeVersion());
11
+ checks.push(checkCommand(npmCommand(), ['--version'], 'npm', true, 'npm'));
12
+ checks.push(checkCommand('git', ['--version'], 'Git', true));
13
+ if (!applicationRoot) {
14
+ checks.push({
15
+ id: 'application',
16
+ status: 'warning',
17
+ message: 'No se ha encontrado una aplicación Open Mova.',
18
+ detail: 'Ejecuta el comando dentro de un directorio que contenga mova.config.json.',
19
+ });
20
+ return { checks };
21
+ }
22
+ checks.push({
23
+ id: 'application',
24
+ status: 'ok',
25
+ message: `Aplicación encontrada en ${applicationRoot}.`,
26
+ });
27
+ let configuration;
28
+ try {
29
+ const document = readApplicationConfigurationDocument(applicationRoot);
30
+ configuration = document.configuration;
31
+ checks.push({
32
+ id: 'configuration',
33
+ status: 'ok',
34
+ message: 'mova.config.json tiene un formato válido.',
35
+ });
36
+ if (document.migrations.length > 0) {
37
+ checks.push({
38
+ id: 'configuration-migrations',
39
+ status: 'warning',
40
+ message: 'mova.config.json necesita migraciones.',
41
+ detail: 'Ejecuta mova update para guardarlo con el esquema actual.',
42
+ });
43
+ }
44
+ }
45
+ catch (error) {
46
+ checks.push({
47
+ id: 'configuration',
48
+ status: 'error',
49
+ message: 'mova.config.json no es válido.',
50
+ detail: error instanceof Error ? error.message : undefined,
51
+ });
52
+ return { applicationRoot, checks };
53
+ }
54
+ checks.push(checkDependencies(applicationRoot, 'shell'));
55
+ checks.push(checkShellVersion(configuration.shell?.version));
56
+ const shellPackage = readOptionalPackage(applicationRoot);
57
+ const shellCoreVersion = shellPackage?.dependencies?.['@open-mova/core'];
58
+ for (const microfrontend of configuration.microfrontends) {
59
+ if (microfrontend.sourcePath) {
60
+ const microfrontendRoot = resolve(applicationRoot, microfrontend.sourcePath);
61
+ if (!existsSync(microfrontendRoot)) {
62
+ checks.push({
63
+ id: `microfrontend:${microfrontend.name}:source`,
64
+ status: 'error',
65
+ message: `No existe el directorio del MF ${microfrontend.name}.`,
66
+ detail: microfrontendRoot,
67
+ });
68
+ }
69
+ else {
70
+ checks.push(checkDependencies(microfrontendRoot, `MF ${microfrontend.name}`));
71
+ checks.push(checkCoreCompatibility(microfrontend.name, shellCoreVersion, readOptionalPackage(microfrontendRoot)?.dependencies?.['@open-mova/core']));
72
+ }
73
+ }
74
+ checks.push(checkProductionRemote(microfrontend.name, microfrontend.productionRemoteEntry));
75
+ }
76
+ const hasAndroid = existsSync(join(applicationRoot, 'android'));
77
+ const hasIos = existsSync(join(applicationRoot, 'ios'));
78
+ checks.push({
79
+ id: 'capacitor-platforms',
80
+ status: hasAndroid || hasIos ? 'ok' : 'warning',
81
+ message: hasAndroid || hasIos
82
+ ? `Plataformas añadidas: ${[hasAndroid && 'Android', hasIos && 'iOS'].filter(Boolean).join(', ')}.`
83
+ : 'No hay plataformas nativas añadidas todavía.',
84
+ });
85
+ if (hasAndroid) {
86
+ checks.push(checkAndroidSdk());
87
+ checks.push(checkCommand('adb', ['version'], 'Android Platform Tools (adb)', true));
88
+ checks.push(checkAndroidEmulators());
89
+ checks.push(checkGoogleMapsKey(applicationRoot, configuration.native?.googleMaps?.androidApiKey));
90
+ }
91
+ if (hasIos) {
92
+ if (process.platform !== 'darwin') {
93
+ checks.push({
94
+ id: 'ios-platform',
95
+ status: 'error',
96
+ message: 'La compilación de iOS requiere macOS.',
97
+ });
98
+ }
99
+ else {
100
+ checks.push(checkCommand('xcodebuild', ['-version'], 'Xcode', true));
101
+ checks.push(checkCommand('pod', ['--version'], 'CocoaPods', false));
102
+ }
103
+ checks.push(...checkIosUsageDescriptions(applicationRoot, shellPackage));
104
+ }
105
+ return { applicationRoot, checks };
106
+ }
107
+ function checkNodeVersion() {
108
+ const majorVersion = Number(process.versions.node.split('.')[0]);
109
+ return majorVersion >= 22
110
+ ? { id: 'node', status: 'ok', message: `Node.js ${process.versions.node}.` }
111
+ : {
112
+ id: 'node',
113
+ status: 'error',
114
+ message: `Node.js ${process.versions.node} no es compatible.`,
115
+ detail: 'Instala Node.js 22 o posterior.',
116
+ };
117
+ }
118
+ function checkCommand(command, args, label, required, id = command) {
119
+ const result = spawnSync(command, [...args], { encoding: 'utf8' });
120
+ const available = !result.error && result.status === 0;
121
+ const version = available ? (result.stdout || result.stderr).trim().split(/\r?\n/)[0] : undefined;
122
+ return {
123
+ id: `command:${id}`,
124
+ status: available ? 'ok' : required ? 'error' : 'warning',
125
+ message: available ? `${label}: ${version}.` : `${label} no está disponible.`,
126
+ };
127
+ }
128
+ function checkDependencies(directory, label) {
129
+ const packagePath = join(directory, 'package.json');
130
+ if (!existsSync(packagePath)) {
131
+ return {
132
+ id: `dependencies:${label}`,
133
+ status: 'error',
134
+ message: `${label} no contiene package.json.`,
135
+ };
136
+ }
137
+ return existsSync(join(directory, 'node_modules'))
138
+ ? {
139
+ id: `dependencies:${label}`,
140
+ status: 'ok',
141
+ message: `${label}: dependencias instaladas.`,
142
+ }
143
+ : {
144
+ id: `dependencies:${label}`,
145
+ status: 'warning',
146
+ message: `${label}: faltan las dependencias.`,
147
+ detail: `Ejecuta npm install en ${directory}.`,
148
+ };
149
+ }
150
+ function checkShellVersion(version) {
151
+ return version && /^v\d+\.\d+\.\d+$/.test(version)
152
+ ? { id: 'shell-version', status: 'ok', message: `Shell registrada: ${version}.` }
153
+ : {
154
+ id: 'shell-version',
155
+ status: 'error',
156
+ message: 'La aplicación no tiene una versión estable de shell registrada.',
157
+ };
158
+ }
159
+ function checkCoreCompatibility(microfrontendName, shellVersion, microfrontendVersion) {
160
+ if (!microfrontendVersion) {
161
+ return {
162
+ id: `microfrontend:${microfrontendName}:core`,
163
+ status: 'warning',
164
+ message: `El MF ${microfrontendName} no declara @open-mova/core.`,
165
+ };
166
+ }
167
+ if (microfrontendVersion === '*') {
168
+ return {
169
+ id: `microfrontend:${microfrontendName}:core`,
170
+ status: 'warning',
171
+ message: `El MF ${microfrontendName} necesita concretar su rango de @open-mova/core.`,
172
+ detail: 'Ejecuta mova mf update o registra de nuevo el MF con --core-version.',
173
+ };
174
+ }
175
+ if (!shellVersion || !areCoreRangesCompatible(shellVersion, microfrontendVersion)) {
176
+ return {
177
+ id: `microfrontend:${microfrontendName}:core`,
178
+ status: 'error',
179
+ message: `El MF ${microfrontendName} y la shell usan rangos distintos de @open-mova/core.`,
180
+ detail: `Shell: ${shellVersion ?? 'no declarado'}; MF: ${microfrontendVersion}.`,
181
+ };
182
+ }
183
+ return {
184
+ id: `microfrontend:${microfrontendName}:core`,
185
+ status: 'ok',
186
+ message: `El MF ${microfrontendName} comparte ${microfrontendVersion} de @open-mova/core.`,
187
+ };
188
+ }
189
+ function checkProductionRemote(name, remoteEntry) {
190
+ const isHttps = remoteEntry === undefined ? false : isHttpsUrl(remoteEntry);
191
+ return isHttps
192
+ ? {
193
+ id: `microfrontend:${name}:production`,
194
+ status: 'ok',
195
+ message: `El MF ${name} tiene una URL HTTPS de producción.`,
196
+ }
197
+ : {
198
+ id: `microfrontend:${name}:production`,
199
+ status: 'warning',
200
+ message: `El MF ${name} no tiene una URL HTTPS de producción válida.`,
201
+ };
202
+ }
203
+ function isHttpsUrl(value) {
204
+ try {
205
+ return new URL(value).protocol === 'https:';
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ function checkAndroidSdk() {
212
+ const sdkRoot = process.env['ANDROID_SDK_ROOT'] ?? process.env['ANDROID_HOME'];
213
+ return sdkRoot && existsSync(sdkRoot)
214
+ ? { id: 'android-sdk', status: 'ok', message: `Android SDK: ${sdkRoot}.` }
215
+ : {
216
+ id: 'android-sdk',
217
+ status: 'warning',
218
+ message: 'No se ha detectado ANDROID_SDK_ROOT ni ANDROID_HOME.',
219
+ };
220
+ }
221
+ function checkAndroidEmulators() {
222
+ const result = spawnSync('emulator', ['-list-avds'], { encoding: 'utf8' });
223
+ if (result.error || result.status !== 0) {
224
+ return {
225
+ id: 'android-emulators',
226
+ status: 'warning',
227
+ message: 'No se ha podido consultar el listado de emuladores Android.',
228
+ };
229
+ }
230
+ const emulators = result.stdout.split(/\r?\n/).filter(Boolean);
231
+ return emulators.length > 0
232
+ ? {
233
+ id: 'android-emulators',
234
+ status: 'ok',
235
+ message: `Emuladores Android disponibles: ${emulators.length}.`,
236
+ }
237
+ : {
238
+ id: 'android-emulators',
239
+ status: 'warning',
240
+ message: 'No hay emuladores Android configurados.',
241
+ };
242
+ }
243
+ function checkGoogleMapsKey(applicationRoot, configuredKey) {
244
+ const resourcePath = join(applicationRoot, 'android', 'app', 'src', 'main', 'res', 'values', 'open_mova_google_maps.xml');
245
+ const resource = existsSync(resourcePath) ? readFileSync(resourcePath, 'utf8') : '';
246
+ const hasKey = Boolean(process.env['OPEN_MOVA_GOOGLE_MAPS_ANDROID_API_KEY'] ||
247
+ configuredKey ||
248
+ (resource && !resource.includes('OPEN_MOVA_GOOGLE_MAPS_API_KEY_NOT_CONFIGURED')));
249
+ return hasKey
250
+ ? { id: 'google-maps-key', status: 'ok', message: 'Google Maps tiene una clave configurada.' }
251
+ : {
252
+ id: 'google-maps-key',
253
+ status: 'warning',
254
+ message: 'Google Maps no tiene una clave Android configurada.',
255
+ };
256
+ }
257
+ function checkIosUsageDescriptions(applicationRoot, packageConfiguration) {
258
+ const plistPath = join(applicationRoot, 'ios', 'App', 'App', 'Info.plist');
259
+ if (!existsSync(plistPath)) {
260
+ return [
261
+ {
262
+ id: 'ios-info-plist',
263
+ status: 'error',
264
+ message: 'No se encuentra ios/App/App/Info.plist.',
265
+ },
266
+ ];
267
+ }
268
+ const plist = readFileSync(plistPath, 'utf8');
269
+ const requiredKeys = [];
270
+ if (packageConfiguration?.dependencies?.['@capacitor/camera']) {
271
+ requiredKeys.push('NSCameraUsageDescription', 'NSPhotoLibraryUsageDescription', 'NSPhotoLibraryAddUsageDescription');
272
+ }
273
+ if (packageConfiguration?.dependencies?.['@capacitor/geolocation']) {
274
+ requiredKeys.push('NSLocationWhenInUseUsageDescription');
275
+ }
276
+ const missingKeys = requiredKeys.filter((key) => !plist.includes(`<key>${key}</key>`));
277
+ return missingKeys.length === 0
278
+ ? [
279
+ {
280
+ id: 'ios-info-plist',
281
+ status: 'ok',
282
+ message: 'Info.plist contiene los permisos básicos requeridos.',
283
+ },
284
+ ]
285
+ : [
286
+ {
287
+ id: 'ios-info-plist',
288
+ status: 'warning',
289
+ message: 'Info.plist no contiene todas las descripciones de uso necesarias.',
290
+ detail: `Faltan: ${missingKeys.join(', ')}.`,
291
+ },
292
+ ];
293
+ }
294
+ function readOptionalPackage(directory) {
295
+ const packagePath = join(directory, 'package.json');
296
+ if (!existsSync(packagePath))
297
+ return undefined;
298
+ try {
299
+ return JSON.parse(readFileSync(packagePath, 'utf8'));
300
+ }
301
+ catch {
302
+ return undefined;
303
+ }
304
+ }
@@ -0,0 +1,3 @@
1
+ export declare const MICROFRONTEND_MANIFEST_PATH = "assets/open-mova.manifest.json";
2
+ export declare function readRequiredCoreVersion(projectRoot: string): string;
3
+ export declare function writeMicrofrontendManifest(projectRoot: string, name: string, remoteName: string, requiredCoreVersion: string): void;
@@ -0,0 +1,20 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const MICROFRONTEND_MANIFEST_PATH = 'assets/open-mova.manifest.json';
4
+ export function readRequiredCoreVersion(projectRoot) {
5
+ const packageConfiguration = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
6
+ const requiredVersion = packageConfiguration.dependencies?.['@open-mova/core'];
7
+ if (!requiredVersion) {
8
+ throw new Error(`${projectRoot} debe declarar @open-mova/core en dependencies para definir su compatibilidad.`);
9
+ }
10
+ return requiredVersion;
11
+ }
12
+ export function writeMicrofrontendManifest(projectRoot, name, remoteName, requiredCoreVersion) {
13
+ const manifest = {
14
+ schemaVersion: 1,
15
+ name,
16
+ remoteName,
17
+ core: { requiredVersion: requiredCoreVersion },
18
+ };
19
+ writeFileSync(join(projectRoot, 'src', 'assets', 'open-mova.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
20
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { toDisplayName, toRemoteName } from '../utils/names.js';
4
+ import { readRequiredCoreVersion, writeMicrofrontendManifest } from './microfrontend-manifest.js';
4
5
  export function configureDownloadedMicrofrontend(destination, name, port, profile) {
5
6
  const projectName = `mova-mf-${name}`;
6
7
  const packagePath = join(destination, 'package.json');
@@ -9,7 +10,6 @@ export function configureDownloadedMicrofrontend(destination, name, port, profil
9
10
  packageJson.scripts.start = `ng serve ${projectName}`;
10
11
  packageJson.scripts.build = `ng build ${projectName}`;
11
12
  if (profile === 'minimal') {
12
- delete packageJson.dependencies['@open-mova/core'];
13
13
  writeFileSync(join(destination, 'src/app/app.routes.ts'), minimalRoutes(name));
14
14
  writeFileSync(join(destination, 'src/app/app.config.ts'), minimalAppConfig());
15
15
  }
@@ -27,6 +27,7 @@ export function configureDownloadedMicrofrontend(destination, name, port, profil
27
27
  .replaceAll('mova-demo-microfrontend', `mova-${name}-microfrontend`));
28
28
  const appPath = join(destination, 'src/app/app.ts');
29
29
  writeFileSync(appPath, readFileSync(appPath, 'utf8').replace('mova-demo-microfrontend', `mova-${name}-microfrontend`));
30
+ writeMicrofrontendManifest(destination, name, toRemoteName(name), readRequiredCoreVersion(destination));
30
31
  // El lock heredado ya no representa el proyecto renombrado ni su dependencia local.
31
32
  rmSync(join(destination, 'package-lock.json'), { force: true });
32
33
  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`);
@@ -0,0 +1,17 @@
1
+ import type { MicrofrontendConfiguration, OpenMovaApplicationConfiguration } from '../types.js';
2
+ import { type FileChange } from './project-update.js';
3
+ import { type ShellVersion } from './shell-repository.js';
4
+ export interface MicrofrontendUpdatePlan {
5
+ readonly microfrontend: MicrofrontendConfiguration;
6
+ readonly projectRoot: string;
7
+ readonly currentVersion: string;
8
+ readonly target: ShellVersion;
9
+ readonly targetCoreVersion: string;
10
+ readonly changes: readonly string[];
11
+ readonly conflicts: readonly string[];
12
+ readonly fileChanges: readonly FileChange[];
13
+ readonly packageContent?: string;
14
+ readonly removePackageLock: boolean;
15
+ }
16
+ export declare function createMicrofrontendUpdatePlan(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, name: string, requestedVersion?: string): MicrofrontendUpdatePlan;
17
+ export declare function applyMicrofrontendUpdate(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, plan: MicrofrontendUpdatePlan): void;
@@ -0,0 +1,117 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { writeApplicationConfiguration } from './configuration.js';
5
+ import { readRequiredCoreVersion } from './microfrontend-manifest.js';
6
+ import { configureDownloadedMicrofrontend } from './microfrontend-profile.js';
7
+ import { applyFileChanges, compareManagedFiles, comparePackageConfiguration, requireCleanGitRepository, } from './project-update.js';
8
+ import { downloadTaggedProject, listShellVersions } from './shell-repository.js';
9
+ import { synchronizeShellConfiguration } from './shell-configuration.js';
10
+ const GENERATED_PATHS = new Set(['package.json', 'package-lock.json']);
11
+ export function createMicrofrontendUpdatePlan(applicationRoot, configuration, name, requestedVersion) {
12
+ const microfrontend = configuration.microfrontends.find((entry) => entry.name === name);
13
+ if (!microfrontend)
14
+ throw new Error(`No existe un microfrontal llamado "${name}".`);
15
+ if (!microfrontend.sourcePath || !microfrontend.template) {
16
+ throw new Error(`El MF ${name} no fue creado desde una plantilla gestionada por Open Mova y no se puede actualizar automáticamente.`);
17
+ }
18
+ const targetVersion = requestedVersion ?? listShellVersions()[0];
19
+ if (!targetVersion)
20
+ throw new Error('No hay versiones estables disponibles para actualizar.');
21
+ if (compareVersions(targetVersion, microfrontend.template.version) < 0) {
22
+ throw new Error('mova mf update no realiza downgrades de plantillas.');
23
+ }
24
+ const projectRoot = resolve(applicationRoot, microfrontend.sourcePath);
25
+ const port = readPort(microfrontend.developmentRemoteEntry);
26
+ const temporaryRoot = mkdtempSync(join(tmpdir(), 'open-mova-mf-update-'));
27
+ try {
28
+ const currentDirectory = join(temporaryRoot, 'current');
29
+ const targetDirectory = join(temporaryRoot, 'target');
30
+ const current = downloadTaggedProject('open-mova-mf-template', currentDirectory, microfrontend.template.version);
31
+ const target = downloadTaggedProject('open-mova-mf-template', targetDirectory, targetVersion);
32
+ if (current.commit !== microfrontend.template.commit) {
33
+ throw new Error(`El commit registrado para la plantilla ${microfrontend.template.version} no coincide con su tag.`);
34
+ }
35
+ configureDownloadedMicrofrontend(currentDirectory, microfrontend.name, port, microfrontend.template.profile);
36
+ configureDownloadedMicrofrontend(targetDirectory, microfrontend.name, port, microfrontend.template.profile);
37
+ const conflicts = [];
38
+ const fileChanges = compareManagedFiles(projectRoot, currentDirectory, targetDirectory, conflicts, GENERATED_PATHS);
39
+ const packageContent = comparePackageConfiguration(projectRoot, currentDirectory, targetDirectory, conflicts);
40
+ const targetCoreVersion = readRequiredCoreVersion(targetDirectory);
41
+ const changes = [
42
+ ...fileChanges.map((change) => `${change.content ? 'Actualizar' : 'Eliminar'} ${change.path}`),
43
+ ...(packageContent ? ['Actualizar package.json', 'Regenerar package-lock.json'] : []),
44
+ ...(target.version === microfrontend.template.version
45
+ ? []
46
+ : [`Registrar plantilla ${target.version}`]),
47
+ ...(targetCoreVersion === microfrontend.compatibility.requiredCoreVersion
48
+ ? []
49
+ : [`Actualizar compatibilidad de Core a ${targetCoreVersion}`]),
50
+ ];
51
+ return {
52
+ microfrontend,
53
+ projectRoot,
54
+ currentVersion: microfrontend.template.version,
55
+ target,
56
+ targetCoreVersion,
57
+ changes,
58
+ conflicts,
59
+ fileChanges,
60
+ ...(packageContent ? { packageContent } : {}),
61
+ removePackageLock: packageContent !== undefined,
62
+ };
63
+ }
64
+ finally {
65
+ rmSync(temporaryRoot, { recursive: true, force: true });
66
+ }
67
+ }
68
+ export function applyMicrofrontendUpdate(applicationRoot, configuration, plan) {
69
+ if (plan.conflicts.length > 0) {
70
+ throw new Error('No se puede actualizar el microfrontal mientras existan conflictos.');
71
+ }
72
+ requireCleanGitRepository(applicationRoot);
73
+ const projectPath = relative(applicationRoot, plan.projectRoot);
74
+ if (isAbsolute(projectPath) || projectPath.startsWith('..')) {
75
+ requireCleanGitRepository(plan.projectRoot);
76
+ }
77
+ applyFileChanges(plan.projectRoot, plan.fileChanges);
78
+ if (plan.packageContent) {
79
+ writeFileSync(join(plan.projectRoot, 'package.json'), plan.packageContent, 'utf8');
80
+ }
81
+ if (plan.removePackageLock) {
82
+ rmSync(join(plan.projectRoot, 'package-lock.json'), { force: true });
83
+ }
84
+ const updatedConfiguration = {
85
+ ...configuration,
86
+ microfrontends: configuration.microfrontends.map((entry) => entry.name === plan.microfrontend.name
87
+ ? {
88
+ ...entry,
89
+ compatibility: { requiredCoreVersion: plan.targetCoreVersion },
90
+ template: {
91
+ ...plan.target,
92
+ project: 'open-mova-mf-template',
93
+ profile: plan.microfrontend.template.profile,
94
+ },
95
+ }
96
+ : entry),
97
+ };
98
+ writeApplicationConfiguration(applicationRoot, updatedConfiguration);
99
+ synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
100
+ }
101
+ function readPort(remoteEntry) {
102
+ const port = Number(new URL(remoteEntry).port);
103
+ if (!Number.isInteger(port) || port < 1) {
104
+ throw new Error(`No se puede obtener el puerto de desarrollo desde ${remoteEntry}.`);
105
+ }
106
+ return port;
107
+ }
108
+ function compareVersions(first, second) {
109
+ const firstParts = first.slice(1).split('.').map(Number);
110
+ const secondParts = second.slice(1).split('.').map(Number);
111
+ for (let index = 0; index < 3; index += 1) {
112
+ const difference = (firstParts[index] ?? 0) - (secondParts[index] ?? 0);
113
+ if (difference !== 0)
114
+ return difference;
115
+ }
116
+ return 0;
117
+ }
@@ -17,6 +17,7 @@ export interface ExistingMicrofrontendOptions {
17
17
  readonly remoteEntry?: string;
18
18
  readonly productionRemoteEntry?: string;
19
19
  readonly port?: number;
20
+ readonly coreVersion?: string;
20
21
  }
21
22
  export declare function createMicrofrontend(applicationRoot: string, configuration: OpenMovaApplicationConfiguration, options: CreateMicrofrontendOptions): MicrofrontendConfiguration;
22
23
  export declare function inspectExistingMicrofrontend(applicationRoot: string, options: ExistingMicrofrontendOptions): MicrofrontendConfiguration;
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readFileSync, rmSync } from 'node:fs';
2
2
  import { relative, resolve, sep } from 'node:path';
3
3
  import { normalizeName, toRemoteName } from '../utils/names.js';
4
- import { configureDownloadedMicrofrontend } from './microfrontend-profile.js';
4
+ import { configureDownloadedMicrofrontend, } from './microfrontend-profile.js';
5
5
  import { downloadTaggedProject } from './shell-repository.js';
6
+ import { readRequiredCoreVersion } from './microfrontend-manifest.js';
6
7
  export function createMicrofrontend(applicationRoot, configuration, options) {
7
8
  const name = normalizeName(options.name, 'El nombre del microfrontal');
8
9
  const route = normalizeName(options.route ?? name, 'La ruta del microfrontal');
@@ -32,6 +33,9 @@ export function createMicrofrontend(applicationRoot, configuration, options) {
32
33
  ? { productionRemoteEntry: options.productionRemoteEntry }
33
34
  : {}),
34
35
  sourcePath: toConfigurationPath(applicationRoot, destination),
36
+ compatibility: {
37
+ requiredCoreVersion: readRequiredCoreVersion(destination),
38
+ },
35
39
  template: { ...template, project: 'open-mova-mf-template', profile },
36
40
  };
37
41
  }
@@ -44,7 +48,7 @@ export function inspectExistingMicrofrontend(applicationRoot, options) {
44
48
  : undefined;
45
49
  const discovered = sourceDirectory
46
50
  ? inspectLocalProject(sourceDirectory)
47
- : { remoteName: undefined, port: undefined };
51
+ : { remoteName: undefined, port: undefined, requiredCoreVersion: undefined };
48
52
  const remoteName = options.remoteName ?? discovered.remoteName;
49
53
  const inferredName = remoteName?.replace(/-microfrontend$/, '');
50
54
  const name = normalizeName(options.name ?? inferredName ?? '', 'El nombre del microfrontal');
@@ -59,6 +63,12 @@ export function inspectExistingMicrofrontend(applicationRoot, options) {
59
63
  if (!remoteEntry) {
60
64
  throw new Error('No se ha podido detectar el puerto. Indica --remote-entry o --port.');
61
65
  }
66
+ const requiredCoreVersion = options.coreVersion ??
67
+ discovered.requiredCoreVersion ??
68
+ readRequiredCoreVersion(applicationRoot);
69
+ if (!requiredCoreVersion) {
70
+ throw new Error('Indica --core-version para declarar qué versión de @open-mova/core necesita el microfrontal remoto.');
71
+ }
62
72
  return {
63
73
  name,
64
74
  route,
@@ -71,6 +81,7 @@ export function inspectExistingMicrofrontend(applicationRoot, options) {
71
81
  ...(sourceDirectory
72
82
  ? { sourcePath: toConfigurationPath(applicationRoot, sourceDirectory) }
73
83
  : {}),
84
+ compatibility: { requiredCoreVersion },
74
85
  };
75
86
  }
76
87
  function inspectLocalProject(directory) {
@@ -85,7 +96,7 @@ function inspectLocalProject(directory) {
85
96
  const federationContent = readFileSync(federationConfiguration, 'utf8');
86
97
  const remoteName = federationContent.match(/\bname\s*:\s*['"]([^'"]+)['"]/)?.[1];
87
98
  const port = readPort(angularConfiguration);
88
- return { remoteName, port };
99
+ return { remoteName, port, requiredCoreVersion: readRequiredCoreVersion(directory) };
89
100
  }
90
101
  function readPort(configurationPath) {
91
102
  const parsed = JSON.parse(readFileSync(configurationPath, 'utf8'));
@@ -0,0 +1,8 @@
1
+ export interface FileChange {
2
+ readonly path: string;
3
+ readonly content?: Buffer;
4
+ }
5
+ export declare function compareManagedFiles(projectRoot: string, currentDirectory: string, targetDirectory: string, conflicts: string[], ignoredPaths: ReadonlySet<string>): FileChange[];
6
+ export declare function comparePackageConfiguration(projectRoot: string, currentDirectory: string, targetDirectory: string, conflicts: string[]): string | undefined;
7
+ export declare function applyFileChanges(projectRoot: string, changes: readonly FileChange[]): void;
8
+ export declare function requireCleanGitRepository(projectRoot: string): void;
@@ -0,0 +1,106 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, relative } from 'node:path';
4
+ export function compareManagedFiles(projectRoot, currentDirectory, targetDirectory, conflicts, ignoredPaths) {
5
+ const paths = new Set([...listFiles(currentDirectory), ...listFiles(targetDirectory)]);
6
+ const changes = [];
7
+ for (const path of [...paths].sort()) {
8
+ if (ignoredPaths.has(path))
9
+ continue;
10
+ const currentContent = readOptionalFile(join(currentDirectory, path));
11
+ const targetContent = readOptionalFile(join(targetDirectory, path));
12
+ const projectContent = readOptionalFile(join(projectRoot, path));
13
+ if (buffersEqual(currentContent, targetContent))
14
+ continue;
15
+ if (buffersEqual(projectContent, currentContent)) {
16
+ changes.push({ path, ...(targetContent ? { content: targetContent } : {}) });
17
+ continue;
18
+ }
19
+ if (buffersEqual(projectContent, targetContent))
20
+ continue;
21
+ conflicts.push(path);
22
+ }
23
+ return changes;
24
+ }
25
+ export function comparePackageConfiguration(projectRoot, currentDirectory, targetDirectory, conflicts) {
26
+ const project = readJson(join(projectRoot, 'package.json'));
27
+ const current = readJson(join(currentDirectory, 'package.json'));
28
+ const target = readJson(join(targetDirectory, 'package.json'));
29
+ let changed = false;
30
+ for (const section of ['scripts', 'dependencies', 'devDependencies']) {
31
+ const projectSection = readStringMap(project[section]);
32
+ const currentSection = readStringMap(current[section]);
33
+ const targetSection = readStringMap(target[section]);
34
+ const keys = new Set([...Object.keys(currentSection), ...Object.keys(targetSection)]);
35
+ for (const key of keys) {
36
+ if (currentSection[key] === targetSection[key])
37
+ continue;
38
+ if (projectSection[key] === currentSection[key]) {
39
+ if (targetSection[key] === undefined) {
40
+ delete projectSection[key];
41
+ }
42
+ else {
43
+ projectSection[key] = targetSection[key];
44
+ }
45
+ changed = true;
46
+ }
47
+ else if (projectSection[key] !== targetSection[key]) {
48
+ conflicts.push(`package.json#${section}.${key}`);
49
+ }
50
+ }
51
+ project[section] = projectSection;
52
+ }
53
+ return changed ? `${JSON.stringify(project, null, 2)}\n` : undefined;
54
+ }
55
+ export function applyFileChanges(projectRoot, changes) {
56
+ for (const change of changes) {
57
+ const destination = join(projectRoot, change.path);
58
+ if (change.content) {
59
+ mkdirSync(dirname(destination), { recursive: true });
60
+ writeFileSync(destination, change.content);
61
+ }
62
+ else {
63
+ rmSync(destination, { force: true });
64
+ }
65
+ }
66
+ }
67
+ export function requireCleanGitRepository(projectRoot) {
68
+ const repository = spawnSync('git', ['-C', projectRoot, 'rev-parse', '--is-inside-work-tree'], {
69
+ encoding: 'utf8',
70
+ });
71
+ if (repository.status !== 0) {
72
+ throw new Error('Inicializa Git y crea un commit antes de actualizar.');
73
+ }
74
+ const status = spawnSync('git', ['-C', projectRoot, 'status', '--porcelain', '--untracked-files=all'], { encoding: 'utf8' });
75
+ if (status.status !== 0 || status.stdout.trim() !== '') {
76
+ throw new Error('El repositorio debe estar limpio antes de actualizar.');
77
+ }
78
+ }
79
+ function listFiles(directory) {
80
+ const files = [];
81
+ const visit = (currentDirectory) => {
82
+ for (const entry of readdirSync(currentDirectory, { withFileTypes: true })) {
83
+ const absolutePath = join(currentDirectory, entry.name);
84
+ if (entry.isDirectory())
85
+ visit(absolutePath);
86
+ else if (entry.isFile())
87
+ files.push(relative(directory, absolutePath));
88
+ }
89
+ };
90
+ visit(directory);
91
+ return files;
92
+ }
93
+ function readOptionalFile(path) {
94
+ return existsSync(path) ? readFileSync(path) : undefined;
95
+ }
96
+ function buffersEqual(first, second) {
97
+ return first === undefined ? second === undefined : second !== undefined && first.equals(second);
98
+ }
99
+ function readJson(path) {
100
+ return JSON.parse(readFileSync(path, 'utf8'));
101
+ }
102
+ function readStringMap(value) {
103
+ if (!value || typeof value !== 'object' || Array.isArray(value))
104
+ return {};
105
+ return { ...value };
106
+ }