Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/cli/src/commands/analyze-shared.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
mkdirSync,
mkdtempSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import {
analyzeSharedDependencies,
applySharedRecommendations,
initFwizConfig,
} from '@federation-wizards/core';

describe('fwiz analyze-shared command', () => {
it('analyzes workspace dependencies and applies fixes', () => {
const dir = mkdtempSync(join(tmpdir(), 'fwiz-cli-analyze-'));

writeFileSync(join(dir, 'nx.json'), '{}');
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({ dependencies: { react: '^19.0.0' } }),
);

mkdirSync(join(dir, 'apps', 'shell'), { recursive: true });
mkdirSync(join(dir, 'apps', 'checkout'), { recursive: true });
writeFileSync(join(dir, 'apps', 'shell', 'project.json'), '{}');
writeFileSync(join(dir, 'apps', 'checkout', 'project.json'), '{}');
writeFileSync(
join(dir, 'apps', 'shell', 'package.json'),
JSON.stringify({ dependencies: { react: '^19.0.0' } }),
);
writeFileSync(
join(dir, 'apps', 'checkout', 'package.json'),
JSON.stringify({ dependencies: { react: '^18.2.0' } }),
);

initFwizConfig(dir);

const analysis = analyzeSharedDependencies(dir);

expect(analysis.conflicts).toHaveLength(1);
expect(analysis.recommendedShared.react.requiredVersion).toBe('^19.0.0');

const fixResult = applySharedRecommendations(
dir,
analysis.recommendedShared,
);

expect(fixResult.changes.length).toBeGreaterThan(0);
expect(analysis.scannedPackages.length).toBeGreaterThan(1);
});
});
72 changes: 72 additions & 0 deletions apps/cli/src/commands/analyze-shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Command } from 'commander';

import {
analyzeSharedDependencies,
applySharedRecommendations,
formatSharedAnalysisTable,
} from '@federation-wizards/core';

export function registerAnalyzeSharedCommand(program: Command): void {
program
.command('analyze-shared')
.description(
'Analyze workspace dependencies and recommend Module Federation shared config',
)
.summary('Scan package.json files and suggest shared dependency settings')
.addHelpText(
'after',
`
Examples:
$ fwiz analyze-shared
$ fwiz analyze-shared --json
$ fwiz analyze-shared --fix
$ fwiz analyze-shared --cwd ./my-monorepo

The command scans root, apps/*, and libs/* package.json files, detects
version conflicts, and recommends singleton/eager settings for shared deps.
Use --fix to apply recommendations to fwiz.config.yaml.
`,
)
.option(
'--cwd <path>',
'Workspace directory to analyze (defaults to current working directory)',
process.cwd(),
)
.option('--json', 'Output analysis as JSON')
.option('--fix', 'Apply recommended shared settings to fwiz.config.yaml')
.action((options: { cwd: string; json?: boolean; fix?: boolean }) => {
const result = analyzeSharedDependencies(options.cwd);

if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatSharedAnalysisTable(result));
console.log('');
console.log(
`Scanned ${result.scannedPackages.length} package.json file(s).`,
);
}

if (options.fix) {
if (Object.keys(result.recommendedShared).length === 0) {
console.warn('No shared dependency recommendations to apply.');
return;
}

const fixResult = applySharedRecommendations(
options.cwd,
result.recommendedShared,
);

if (fixResult.changes.length === 0) {
console.log(`No changes needed in ${fixResult.configPath}`);
return;
}

console.log(`Updated ${fixResult.configPath}`);
for (const change of fixResult.changes) {
console.log(` - ${change}`);
}
}
});
}
2 changes: 2 additions & 0 deletions apps/cli/src/program.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Command } from 'commander';

import { registerAnalyzeSharedCommand } from './commands/analyze-shared.js';
import { registerInitCommand } from './commands/init.js';

export function createProgram(): Command {
Expand All @@ -11,6 +12,7 @@ export function createProgram(): Command {
.version('0.0.1');

registerInitCommand(program);
registerAnalyzeSharedCommand(program);

return program;
}
1 change: 1 addition & 0 deletions libs/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
}
},
"dependencies": {
"@federation-wizards/utils": "workspace:*",
"joi": "^18.2.1",
"tslib": "^2.3.0",
"yaml": "^2.9.0"
Expand Down
1 change: 1 addition & 0 deletions libs/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './lib/config/init.js';
export * from './lib/config/load.js';
export * from './lib/workspace/detect.js';
export * from './lib/workspace/patch.js';
export * from './lib/shared/index.js';
126 changes: 126 additions & 0 deletions libs/core/src/lib/shared/analyze.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
mkdirSync,
mkdtempSync,
readFileSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { describe, expect, it } from 'vitest';

import { createDefaultConfig } from '../config/defaults.js';
import { FWIZ_CONFIG_FILENAME } from '../config/init.js';
import {
analyzeSharedDependencies,
applySharedRecommendations,
formatSharedAnalysisTable,
} from './analyze.js';

function createConflictingWorkspace(): string {
const dir = mkdtempSync(join(tmpdir(), 'fwiz-analyze-conflict-'));

writeFileSync(join(dir, 'nx.json'), '{}');
writeFileSync(
join(dir, 'package.json'),
JSON.stringify({ dependencies: { react: '^19.0.0' } }),
);

mkdirSync(join(dir, 'apps', 'shell'), { recursive: true });
mkdirSync(join(dir, 'apps', 'checkout'), { recursive: true });
mkdirSync(join(dir, 'libs', 'ui'), { recursive: true });

writeFileSync(
join(dir, 'apps', 'shell', 'package.json'),
JSON.stringify({
dependencies: { react: '^19.0.0', 'react-dom': '^19.0.0' },
}),
);
writeFileSync(
join(dir, 'apps', 'checkout', 'package.json'),
JSON.stringify({
dependencies: { react: '^18.2.0', 'react-dom': '^18.2.0' },
}),
);
writeFileSync(
join(dir, 'libs', 'ui', 'package.json'),
JSON.stringify({
peerDependencies: { react: '^19.0.0' },
}),
);

const config = createDefaultConfig({
type: 'nx',
appProjects: ['shell', 'checkout'],
reactVersion: '^18.2.0',
});

writeFileSync(
join(dir, FWIZ_CONFIG_FILENAME),
`${stringifyYaml(config)}\n`,
'utf8',
);

return dir;
}

describe('analyzeSharedDependencies', () => {
it('detects common dependencies and version conflicts', () => {
const dir = createConflictingWorkspace();
const result = analyzeSharedDependencies(dir);

expect(result.scannedPackages).toEqual([
'apps/checkout/package.json',
'apps/shell/package.json',
'libs/ui/package.json',
'package.json',
]);

const react = result.sharedDependencies.find(
(dependency) => dependency.name === 'react',
);

expect(react?.projectCount).toBe(4);
expect(react?.hasConflict).toBe(true);
expect(react?.recommended.singleton).toBe(true);
expect(react?.recommended.requiredVersion).toBe('^19.0.0');

expect(result.conflicts).toHaveLength(2);
expect(result.recommendedShared.react.requiredVersion).toBe('^19.0.0');
});

it('formats a readable console table', () => {
const dir = createConflictingWorkspace();
const result = analyzeSharedDependencies(dir);
const table = formatSharedAnalysisTable(result);

expect(table).toContain('Package');
expect(table).toContain('react');
expect(table).toContain('Version conflicts:');
expect(table).toContain('Align all projects to ^19.0.0');
});
});

describe('applySharedRecommendations', () => {
it('updates fwiz.config.yaml with recommended shared settings', () => {
const dir = createConflictingWorkspace();
const analysis = analyzeSharedDependencies(dir);
const fixResult = applySharedRecommendations(
dir,
analysis.recommendedShared,
);

expect(fixResult.changes.length).toBeGreaterThan(0);

const config = parseYaml(
readFileSync(join(dir, FWIZ_CONFIG_FILENAME), 'utf8'),
) as {
shared: Record<string, { requiredVersion: string; singleton: boolean }>;
};

expect(config.shared.react.requiredVersion).toBe('^19.0.0');
expect(config.shared.react.singleton).toBe(true);
expect(config.shared['react-dom'].requiredVersion).toBe('^19.0.0');
});
});
Loading
Loading