Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added download-federated-types script #37

Merged
merged 1 commit into from
Jan 8, 2024
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ node_modules
# Mac
*.DS_Store
**/*.DS_Store

# Tests
coverage
report.json
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,21 @@ Or it can be added to `package.json`:

### CLI options

| Option | Default value | Description |
|-------------------------------|---------------|--------------------------------------------------------------------------------|
| `--output-types-folder`, `-o` | `dist/@types` | Path to the output folder, absolute or relative to the working directory |
| `--global-types`, `-g` | `src/@types` | Path to project's global ambient type definitions, relative to the working dir |
| `--federation-config`, `-c` | `src/@types` | Path to federation.config, relative to the working dir |
| `--tsconfig`, `-t` | `src/@types` | Path to tsconfig.json, relative to the working dir |
#### download-federated-types
| Option | Default value | Description |
|-------------------------------|-------------------|---------------------------|
| `--webpack-config` | `webpack/prod.ts` | Path to webpack-config.js |

If the config is written in TypeScript, the script should be called with `npx ts-node`.

#### make-federated-types
| Option | Default value | Description |
|-------------------------------|-------------------|--------------------------------------------------------------------------------|
| `--output-types-folder`, `-o` | `dist/@types` | Path to the output folder, absolute or relative to the working directory |
| `--global-types`, `-g` | `src/@types` | Path to project's global ambient type definitions, relative to the working dir |
| `--federation-config`, `-c` | `src/@types` | Path to federation.config, relative to the working dir |
| `--tsconfig`, `-t` | `src/@types` | Path to tsconfig.json, relative to the working dir |
| `--webpack-config` | `webpack/prod.ts` | Path to webpack-config.js |

## Plugin Configuration

Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export default {
preset: 'ts-jest',
rootDir: 'src',
clearMocks: true,
coverageDirectory: '<rootDir>/../coverage',
};
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
"dist"
],
"bin": {
"make-federated-types": "dist/make-federated-types.js",
"download-federated-types": "dist/download-federated-types.js"
"make-federated-types": "dist/bin/make-federated-types.js",
"download-federated-types": "dist/bin/download-federated-types.js"
},
"scripts": {
"build": "tsc",
"lint": "eslint 'src/**/*.ts'",
"test": "jest"
"test": "jest",
"test:coverage": "jest --coverage",
"test:coverage:ci": "jest --verbose --ci --coverage --json --testLocationInResults --outputFile=report.json --maxWorkers=50%"
},
"dependencies": {
"download": "^8.0.0",
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import webpack, {
Compilation, Compiler,
} from 'webpack';

import { downloadTypes } from '../helpers/downloadTypes';
import { downloadTypes } from '../downloadTypes/downloadTypes';
import { ModuleFederationTypesPlugin } from '../plugin';
import {
ModuleFederationPluginOptions, ModuleFederationTypesPluginOptions,
} from '../types';
} from '../models';
import {
DEFAULT_DIR_DOWNLOADED_TYPES, DEFAULT_DIR_EMITTED_TYPES,
} from '../constants';

jest.mock('../helpers/downloadTypes');
jest.mock('../downloadTypes/downloadTypes');

const mockDownloadTypes = downloadTypes as jest.MockedFunction<typeof downloadTypes>;
const mockAfterEmit = jest.fn();
Expand Down
156 changes: 156 additions & 0 deletions src/bin/__tests__/download-federated-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
DEFAULT_DIR_DOWNLOADED_TYPES, DEFAULT_DIR_EMITTED_TYPES,
} from '../../constants';
import {
downloadTypes, getRemoteManifestUrls,
} from '../../downloadTypes';
import { getOptionsFromWebpackConfig } from '../helpers';

jest.mock('minimist', () => (args: string[]) => {
const webpackConfigIndex = args.findIndex(arg => arg === '--webpack-config');
return webpackConfigIndex > -1
? { 'webpack-config': args[webpackConfigIndex + 1] }
: {};
});
jest.mock('../../downloadTypes', () => ({
downloadTypes: jest.fn(),
getRemoteManifestUrls: jest.fn(),
}));
jest.mock('../helpers', () => ({
assertRunningFromRoot: jest.fn(() => true),
getOptionsFromWebpackConfig: jest.fn(),
}));

const mockDownloadTypes = downloadTypes as jest.MockedFunction<typeof downloadTypes>;
const mockGetOptionsFromWebpackConfig = getOptionsFromWebpackConfig as jest
.MockedFunction<typeof getOptionsFromWebpackConfig>;
const mockGetRemoteManifestUrls = getRemoteManifestUrls as jest
.MockedFunction<typeof getRemoteManifestUrls>;

const validOptions: ReturnType<typeof getOptionsFromWebpackConfig> = {
mfPluginOptions: {
remotes: {
app1: 'app1@https://app1-url/remoteEntry.js',
app2: 'app1@https://app2-url/remoteEntry.js',
},
},
mfTypesPluginOptions: {
remoteEntryUrls: { url1: 'http://valid-url' },
dirDownloadedTypes: 'custom-dist/types',
dirEmittedTypes: 'src/@wmf-types/types',
},
};

describe('download-federated-types', () => {
const originalArgv = process.argv;
const originalProcessExit = process.exit;
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
process.exit = jest.fn() as never;

beforeEach(() => {
process.argv = ['node', 'download-federated-types'];

mockGetRemoteManifestUrls.mockReturnValue({});
mockGetOptionsFromWebpackConfig.mockReturnValue({
mfPluginOptions: {},
mfTypesPluginOptions: {},
});
});

afterEach(() => {
process.argv = originalArgv;
});

afterAll(() => {
process.exit = originalProcessExit;
});

test('exits when remote URL is invalid', () => {
const remoteEntryUrls = { url1: 'invalid-url' };
mockGetOptionsFromWebpackConfig.mockReturnValue({
mfPluginOptions: {},
mfTypesPluginOptions: {
remoteEntryUrls,
},
});

jest.isolateModules(() => {
require('../download-federated-types');
});

expect(mockGetOptionsFromWebpackConfig).toHaveBeenCalledWith('webpack/prod.ts');
expect(mockConsoleError).toHaveBeenCalledWith('One or more remote URLs are invalid:', remoteEntryUrls);
expect(process.exit).toHaveBeenCalledWith(1);
});

test('exits when remote manifest URL is invalid', () => {
const manifestUrls = { url1: 'invalid-url' };
mockGetRemoteManifestUrls.mockReturnValue(manifestUrls);

jest.isolateModules(() => {
require('../download-federated-types');
});

expect(mockGetRemoteManifestUrls).toHaveBeenCalled();
expect(process.exit).toHaveBeenCalledWith(1);
expect(mockConsoleError).toHaveBeenCalledWith('One or more remote manifest URLs are invalid:', manifestUrls);
});

test('calls downloadTypes with correct arguments and logs success on valid URLs', async () => {
const manifestUrls = { url1: 'https://manifest-registry' };
mockGetRemoteManifestUrls.mockReturnValue(manifestUrls);
mockGetOptionsFromWebpackConfig.mockReturnValue(validOptions);

jest.isolateModules(() => {
require('../download-federated-types');
});
await Promise.resolve();

expect(mockDownloadTypes).toHaveBeenCalledWith(
validOptions.mfTypesPluginOptions.dirEmittedTypes,
validOptions.mfTypesPluginOptions.dirDownloadedTypes,
validOptions.mfPluginOptions.remotes,
validOptions.mfTypesPluginOptions.remoteEntryUrls,
manifestUrls,
);
expect(mockConsoleLog).toHaveBeenCalledWith('Successfully downloaded federated types.');
});

test('exits with error when downloadTypes throws an error', async () => {
mockDownloadTypes.mockRejectedValue(new Error('Error downloading types'));

jest.isolateModules(() => {
require('../download-federated-types');
});
await Promise.resolve();

expect(mockConsoleError).toHaveBeenCalledWith('Error downloading federated types:', expect.any(Error));
expect(process.exit).toHaveBeenCalledWith(1);
});

test('uses default directories when mfTypesPluginOptions does not provide them', () => {
jest.isolateModules(() => {
require('../download-federated-types');
});

expect(mockDownloadTypes).toHaveBeenCalledWith(
DEFAULT_DIR_EMITTED_TYPES,
DEFAULT_DIR_DOWNLOADED_TYPES,
undefined,
undefined,
{},
);
});

test('parses argv and uses custom webpack config path', () => {
process.argv[2] = '--webpack-config';
process.argv[3] = 'custom/webpack.config.ts';

jest.isolateModules(() => {
require('../download-federated-types');
});

expect(mockGetOptionsFromWebpackConfig).toHaveBeenCalledWith('custom/webpack.config.ts');
});
});
57 changes: 57 additions & 0 deletions src/bin/download-federated-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env node

import parseArgs from 'minimist';

import {
DEFAULT_DIR_DOWNLOADED_TYPES, DEFAULT_DIR_EMITTED_TYPES,
} from '../constants';
import {
downloadTypes, getRemoteManifestUrls,
} from '../downloadTypes';
import {
isEveryUrlValid, setLogger,
} from '../helpers';

import {
assertRunningFromRoot, getOptionsFromWebpackConfig,
} from './helpers';

assertRunningFromRoot();

type Argv = {
'webpack-config'?: string,
};

const argv = parseArgs<Argv>(process.argv.slice(2));
const webpackConfigPath = argv['webpack-config'] || 'webpack/prod.ts';

const { mfPluginOptions, mfTypesPluginOptions } = getOptionsFromWebpackConfig(webpackConfigPath);

const remoteManifestUrls = getRemoteManifestUrls(mfTypesPluginOptions)!;

if (!isEveryUrlValid(Object.values({ ...mfTypesPluginOptions.remoteEntryUrls }))) {
console.error('One or more remote URLs are invalid:', mfTypesPluginOptions.remoteEntryUrls);
process.exit(1);
}
if (!isEveryUrlValid(Object.values({ ...remoteManifestUrls }))) {
console.error('One or more remote manifest URLs are invalid:', remoteManifestUrls);
process.exit(1);
}

(async () => {
setLogger(console);

try {
await downloadTypes(
mfTypesPluginOptions?.dirEmittedTypes || DEFAULT_DIR_EMITTED_TYPES,
mfTypesPluginOptions?.dirDownloadedTypes || DEFAULT_DIR_DOWNLOADED_TYPES,
mfPluginOptions.remotes,
mfTypesPluginOptions.remoteEntryUrls,
remoteManifestUrls,
);
console.log('Successfully downloaded federated types.');
} catch (error) {
console.error('Error downloading federated types:', error);
process.exit(1);
}
})();
8 changes: 8 additions & 0 deletions src/bin/helpers/assertRunningFromRoot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import fs from 'fs';

export function assertRunningFromRoot(): void {
if (!fs.readdirSync('./').includes('node_modules')) {
console.error('ERROR: Script must be run from the root directory of the project');
process.exit(1);
}
}
12 changes: 2 additions & 10 deletions src/helpers/cli.ts → src/bin/helpers/getFederationConfig.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import fs from 'fs';
import path from 'path';

import { FEDERATION_CONFIG_FILE } from '../constants';
import { FederationConfig } from '../types';

export function assertRunningFromRoot(): void {
if (!fs.readdirSync('./').includes('node_modules')) {
console.error('ERROR: Script must be run from the root directory of the project');
process.exit(1);
}
}
import { FEDERATION_CONFIG_FILE } from '../../constants';
import { FederationConfig } from '../../models';

export function getFederationConfig(customConfigPath?: string): FederationConfig {
const federationConfigPath = path.resolve(customConfigPath || FEDERATION_CONFIG_FILE);
Expand Down
47 changes: 47 additions & 0 deletions src/bin/helpers/getOptionsFromWebpackConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Compiler } from 'webpack';

import { ModuleFederationTypesPluginOptions } from '../../models';

export function getOptionsFromWebpackConfig(webpackConfigPath: string) {
let webpackConfig: Compiler['options'];
try {
webpackConfig = require(webpackConfigPath);
webpackConfig = ((webpackConfig as unknown as Dict).default as Compiler['options']) || webpackConfig;
} catch (error) {
console.error(`Failed to import webpack config from ${webpackConfigPath}:`, error);
process.exit(1);
}

if (!webpackConfig) {
console.error(`Empty webpack config loaded from ${webpackConfigPath}`);
process.exit(1);
}

function getModuleFederationPluginOptions(config: Compiler['options']) {
const plugin = config.plugins.find(
nextPlugin => nextPlugin!.constructor.name === 'ModuleFederationPlugin',
);
// eslint-disable-next-line no-underscore-dangle
return (plugin as Dict)?._options as Dict & { remotes?: Dict<string> };
}

function getModuleFederationTypesPluginOptions(config: Compiler['options']) {
const plugin = config.plugins.find(
nextPlugin => nextPlugin!.constructor.name === 'ModuleFederationTypesPlugin',
);
return (plugin as Dict)?.options as ModuleFederationTypesPluginOptions;
}

const mfPluginOptions = getModuleFederationPluginOptions(webpackConfig);
const mfTypesPluginOptions = getModuleFederationTypesPluginOptions(webpackConfig);

if (!mfTypesPluginOptions || !mfPluginOptions) {
console.error('Could not find required ModuleFederation plugin options in the webpack config.');
process.exit(1);
}

return {
mfPluginOptions,
mfTypesPluginOptions,
};
}
3 changes: 3 additions & 0 deletions src/bin/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './assertRunningFromRoot';
export * from './getFederationConfig';
export * from './getOptionsFromWebpackConfig';
Loading