@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
@@ -0,0 +1,143 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { npmCommand, useCommandShell } from '../utils/platform.js';
5
+ import { readApplicationConfiguration, writeApplicationConfiguration } from './configuration.js';
6
+ import { readNativeCapabilityCatalog, synchronizeShellConfiguration, } from './shell-configuration.js';
7
+ export function listNativeCapabilities(applicationRoot) {
8
+ const configuration = readApplicationConfiguration(applicationRoot);
9
+ const enabled = new Set(configuration.native?.capabilities ?? []);
10
+ return readNativeCapabilityCatalog(applicationRoot).capabilities.map((definition) => ({
11
+ definition,
12
+ enabled: enabled.has(definition.name),
13
+ }));
14
+ }
15
+ export function enableNativeCapabilities(applicationRoot, requestedNames) {
16
+ const configuration = readApplicationConfiguration(applicationRoot);
17
+ const definitions = resolveDefinitions(applicationRoot, requestedNames);
18
+ const enabled = new Set(configuration.native?.capabilities ?? []);
19
+ const newDefinitions = definitions.filter((definition) => !enabled.has(definition.name));
20
+ for (const definition of definitions)
21
+ enabled.add(definition.name);
22
+ installPackages(applicationRoot, newDefinitions);
23
+ return persistCapabilities(applicationRoot, configuration, [...enabled]);
24
+ }
25
+ export function disableNativeCapabilities(applicationRoot, requestedNames) {
26
+ const configuration = readApplicationConfiguration(applicationRoot);
27
+ const definitions = resolveDefinitions(applicationRoot, requestedNames);
28
+ const currentlyEnabled = new Set(configuration.native?.capabilities ?? []);
29
+ const activeDefinitions = definitions.filter((definition) => currentlyEnabled.has(definition.name));
30
+ const disabled = new Set(definitions.map((definition) => definition.name));
31
+ const remainingNames = (configuration.native?.capabilities ?? []).filter((name) => !disabled.has(name));
32
+ const remainingDefinitions = remainingNames.length > 0 ? resolveDefinitions(applicationRoot, remainingNames) : [];
33
+ const retainedPackages = new Set(remainingDefinitions.map((definition) => definition.package));
34
+ const packagesToRemove = [
35
+ ...new Set(activeDefinitions
36
+ .map((definition) => definition.package)
37
+ .filter((packageName) => packageName !== '@capacitor/core')),
38
+ ].filter((packageName) => !retainedPackages.has(packageName));
39
+ uninstallPackages(applicationRoot, packagesToRemove);
40
+ return persistCapabilities(applicationRoot, configuration, remainingNames);
41
+ }
42
+ export function diagnoseNativeCapabilities(applicationRoot, platform) {
43
+ const statuses = listNativeCapabilities(applicationRoot).filter((status) => status.enabled);
44
+ const messages = [];
45
+ if (statuses.length === 0) {
46
+ return ['No hay capacidades nativas habilitadas.'];
47
+ }
48
+ for (const { definition } of statuses) {
49
+ if (platform && !definition.platforms.includes(platform)) {
50
+ messages.push(`⚠ ${definition.name}: no es compatible con ${platform}.`);
51
+ continue;
52
+ }
53
+ const requirements = [];
54
+ if (platform === 'android' && definition.minimumAndroidSdk) {
55
+ requirements.push(`Android SDK mínimo ${definition.minimumAndroidSdk}`);
56
+ }
57
+ if (platform && definition.permissions?.[platform]?.length) {
58
+ requirements.push(`permisos: ${definition.permissions[platform]?.join(', ')}`);
59
+ }
60
+ if (definition.notes?.length)
61
+ requirements.push(...definition.notes);
62
+ messages.push(requirements.length > 0
63
+ ? `• ${definition.name}: ${requirements.join('; ')}`
64
+ : `✓ ${definition.name}: sin configuración adicional declarada${platform ? ` para ${platform}` : ''}.`);
65
+ }
66
+ return messages;
67
+ }
68
+ /**
69
+ * Recompone las dependencias seleccionables después de actualizar la shell.
70
+ * No instala paquetes: el flujo de update regenera después el lockfile.
71
+ */
72
+ export function synchronizeNativeCapabilityDependencies(applicationRoot, configuration) {
73
+ const packagePath = join(applicationRoot, 'package.json');
74
+ const packageConfiguration = JSON.parse(readFileSync(packagePath, 'utf8'));
75
+ const dependencies = { ...(packageConfiguration.dependencies ?? {}) };
76
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
77
+ const enabled = new Set(configuration.native?.capabilities ?? []);
78
+ const selectablePackages = new Set(catalog.capabilities
79
+ .map((definition) => definition.package)
80
+ .filter((packageName) => packageName !== '@capacitor/core'));
81
+ for (const packageName of selectablePackages)
82
+ delete dependencies[packageName];
83
+ for (const definition of catalog.capabilities) {
84
+ if (enabled.has(definition.name) &&
85
+ definition.package !== '@capacitor/core' &&
86
+ definition.version) {
87
+ dependencies[definition.package] = definition.version;
88
+ }
89
+ }
90
+ packageConfiguration.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([first], [second]) => first.localeCompare(second)));
91
+ writeFileSync(packagePath, `${JSON.stringify(packageConfiguration, null, 2)}\n`, 'utf8');
92
+ }
93
+ function persistCapabilities(applicationRoot, configuration, capabilities) {
94
+ const updated = {
95
+ ...configuration,
96
+ native: {
97
+ ...configuration.native,
98
+ capabilities: [...new Set(capabilities)].sort(),
99
+ },
100
+ };
101
+ writeApplicationConfiguration(applicationRoot, updated);
102
+ synchronizeShellConfiguration(applicationRoot, updated);
103
+ return updated;
104
+ }
105
+ function resolveDefinitions(applicationRoot, names) {
106
+ if (names.length === 0)
107
+ throw new Error('Indica al menos una capacidad nativa.');
108
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
109
+ const definitionsByName = new Map(catalog.capabilities.map((definition) => [definition.name, definition]));
110
+ const unknown = names.filter((name) => !definitionsByName.has(name));
111
+ if (unknown.length > 0) {
112
+ throw new Error(`Capacidades desconocidas: ${unknown.join(', ')}. Usa "mova cap list" para consultar el catálogo.`);
113
+ }
114
+ return names.map((name) => definitionsByName.get(name));
115
+ }
116
+ function installPackages(applicationRoot, definitions) {
117
+ const packages = [
118
+ ...new Set(definitions
119
+ .filter((definition) => definition.package !== '@capacitor/core')
120
+ .map((definition) => `${definition.package}@${definition.version ?? 'latest'}`)),
121
+ ];
122
+ if (packages.length > 0)
123
+ runNpm(applicationRoot, ['install', '--save', ...packages]);
124
+ }
125
+ function uninstallPackages(applicationRoot, packages) {
126
+ if (packages.length > 0)
127
+ runNpm(applicationRoot, ['uninstall', '--save', ...packages]);
128
+ }
129
+ function runNpm(applicationRoot, arguments_) {
130
+ if (!existsSync(join(applicationRoot, 'package.json'))) {
131
+ throw new Error(`No se encuentra package.json en ${applicationRoot}.`);
132
+ }
133
+ const result = spawnSync(npmCommand(), arguments_, {
134
+ cwd: applicationRoot,
135
+ stdio: 'inherit',
136
+ shell: useCommandShell(),
137
+ });
138
+ if (result.error)
139
+ throw result.error;
140
+ if (result.status !== 0) {
141
+ throw new Error(`${npmCommand()} ${arguments_.join(' ')} terminó con error.`);
142
+ }
143
+ }
@@ -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
+ }
@@ -1,2 +1,22 @@
1
1
  import type { OpenMovaApplicationConfiguration } from '../types.js';
2
+ export interface NativeCapabilityDefinition {
3
+ readonly name: string;
4
+ readonly implementation: string;
5
+ readonly exportName: string;
6
+ readonly package: string;
7
+ readonly version?: string;
8
+ readonly platforms: readonly ('android' | 'ios' | 'web')[];
9
+ readonly minimumAndroidSdk?: number;
10
+ readonly permissions?: {
11
+ readonly android?: readonly string[];
12
+ readonly ios?: readonly string[];
13
+ };
14
+ readonly notes?: readonly string[];
15
+ }
16
+ interface NativeCapabilityCatalog {
17
+ readonly schemaVersion: 1;
18
+ readonly capabilities: readonly NativeCapabilityDefinition[];
19
+ }
2
20
  export declare function synchronizeShellConfiguration(applicationRoot: string, configuration: OpenMovaApplicationConfiguration): void;
21
+ export declare function readNativeCapabilityCatalog(applicationRoot: string): NativeCapabilityCatalog;
22
+ export {};
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  export function synchronizeShellConfiguration(applicationRoot, configuration) {
4
4
  const manifest = Object.fromEntries(configuration.microfrontends.map((microfrontend) => [
@@ -7,6 +7,55 @@ export function synchronizeShellConfiguration(applicationRoot, configuration) {
7
7
  ]));
8
8
  writeFileSync(join(applicationRoot, 'src', 'assets', 'federation.manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
9
9
  writeFileSync(join(applicationRoot, 'src', 'app', 'application.config.ts'), renderApplicationConfiguration(configuration), 'utf8');
10
+ synchronizeNativeCapabilities(applicationRoot, configuration);
11
+ }
12
+ export function readNativeCapabilityCatalog(applicationRoot) {
13
+ const catalogPath = join(applicationRoot, 'native-capabilities.catalog.json');
14
+ const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
15
+ if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.capabilities)) {
16
+ throw new Error(`${catalogPath} no contiene un catálogo de capacidades válido.`);
17
+ }
18
+ return catalog;
19
+ }
20
+ function synchronizeNativeCapabilities(applicationRoot, configuration) {
21
+ const catalog = readNativeCapabilityCatalog(applicationRoot);
22
+ const enabledNames = new Set(configuration.native?.capabilities ?? []);
23
+ const knownNames = new Set(catalog.capabilities.map((capability) => capability.name));
24
+ const unknownNames = [...enabledNames].filter((name) => !knownNames.has(name));
25
+ if (unknownNames.length > 0) {
26
+ throw new Error(`Capacidades nativas desconocidas: ${unknownNames.join(', ')}.`);
27
+ }
28
+ const enabled = catalog.capabilities.filter((capability) => enabledNames.has(capability.name));
29
+ const imports = enabled
30
+ .map((capability) => `import { ${capability.exportName} } from './${capability.implementation}';`)
31
+ .join('\n');
32
+ const entries = catalog.capabilities
33
+ .map((capability) => {
34
+ const value = enabledNames.has(capability.name)
35
+ ? capability.exportName
36
+ : `createUnavailableNativeCapability<NativeCapabilities[${JSON.stringify(capability.name)}]>(${JSON.stringify(capability.name)})`;
37
+ return ` ${capability.name}: ${value},`;
38
+ })
39
+ .join('\n');
40
+ const source = `import type { Provider } from '@angular/core';
41
+ import { NATIVE_CAPABILITIES, type NativeCapabilities } from '@open-mova/core';
42
+ import { createUnavailableNativeCapability } from './unavailable-native-capability';
43
+ ${imports ? `\n${imports}` : ''}
44
+
45
+ // Generado por el CLI desde mova.config.json. No editar manualmente.
46
+ const nativeCapabilities = {
47
+ version: 1,
48
+ ${entries}
49
+ } satisfies NativeCapabilities;
50
+
51
+ export function provideNativeCapabilities(): Provider {
52
+ return {
53
+ provide: NATIVE_CAPABILITIES,
54
+ useValue: nativeCapabilities,
55
+ };
56
+ }
57
+ `;
58
+ writeFileSync(join(applicationRoot, 'src', 'native-capabilities', 'native-capabilities.provider.ts'), source, 'utf8');
10
59
  }
11
60
  function renderApplicationConfiguration(configuration) {
12
61
  const entries = configuration.microfrontends
@@ -14,12 +63,14 @@ function renderApplicationConfiguration(configuration) {
14
63
  path: ${JSON.stringify(microfrontend.route)},
15
64
  remote: ${JSON.stringify(microfrontend.remoteName)},
16
65
  exposedModule: './Routes',
66
+ requiredCoreVersion: ${JSON.stringify(microfrontend.compatibility.requiredCoreVersion)},
17
67
  },`)
18
68
  .join('\n');
19
69
  return `export interface MicrofrontendDefinition {
20
70
  readonly path: string;
21
71
  readonly remote: string;
22
72
  readonly exposedModule: './Routes';
73
+ readonly requiredCoreVersion: string;
23
74
  }
24
75
 
25
76
  // 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,13 @@
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';
10
+ import { synchronizeNativeCapabilityDependencies } from './native-capabilities.js';
8
11
  const GENERATED_PATHS = new Set([
9
12
  'package.json',
10
13
  'package-lock.json',
@@ -34,8 +37,10 @@ export function createShellUpdatePlan(applicationRoot, configuration, requestedV
34
37
  throw new Error(`El commit registrado para ${configuration.shell.version} no coincide con el tag remoto.`);
35
38
  }
36
39
  const conflicts = [];
37
- const fileChanges = compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts);
40
+ const fileChanges = compareManagedFiles(applicationRoot, currentDirectory, targetDirectory, conflicts, GENERATED_PATHS);
38
41
  const packageUpdate = comparePackageConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
42
+ const targetCoreVersion = readRequiredCoreVersion(targetDirectory);
43
+ conflicts.push(...findIncompatibleMicrofrontends(configuration, targetCoreVersion));
39
44
  const capacitorUpdate = compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts);
40
45
  const changes = [
41
46
  ...fileChanges.map((change) => `${change.content ? 'Actualizar' : 'Eliminar'} ${change.path}`),
@@ -65,16 +70,7 @@ export function applyShellUpdate(applicationRoot, configuration, plan) {
65
70
  throw new Error('No se puede actualizar mientras existan conflictos.');
66
71
  }
67
72
  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
- }
73
+ applyFileChanges(applicationRoot, plan.fileChanges);
78
74
  if (plan.packageContent) {
79
75
  writeFileSync(join(applicationRoot, 'package.json'), plan.packageContent, 'utf8');
80
76
  }
@@ -88,63 +84,10 @@ export function applyShellUpdate(applicationRoot, configuration, plan) {
88
84
  ...configuration,
89
85
  shell: plan.target,
90
86
  };
87
+ synchronizeNativeCapabilityDependencies(applicationRoot, updatedConfiguration);
91
88
  writeApplicationConfiguration(applicationRoot, updatedConfiguration);
92
89
  synchronizeShellConfiguration(applicationRoot, updatedConfiguration);
93
90
  }
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
91
  function compareCapacitorConfiguration(applicationRoot, currentDirectory, targetDirectory, conflicts) {
149
92
  const applicationPath = join(applicationRoot, 'capacitor.config.ts');
150
93
  const currentPath = join(currentDirectory, 'capacitor.config.ts');
@@ -163,46 +106,6 @@ function compareCapacitorConfiguration(applicationRoot, currentDirectory, target
163
106
  conflicts.push('capacitor.config.ts');
164
107
  return undefined;
165
108
  }
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
109
  function readCapacitorIdentity(content) {
207
110
  const appId = content.match(/\bappId:\s*['"]([^'"]+)['"]/)?.[1];
208
111
  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);