Skip to content

Commit

Permalink
Merge pull request #37 from cloudbeds/feat/download-federated-types
Browse files Browse the repository at this point in the history
feat: added download-federated-types script
  • Loading branch information
steven-pribilinskiy committed Jan 8, 2024
2 parents b969be0 + 15a3500 commit 22eae52
Show file tree
Hide file tree
Showing 46 changed files with 907 additions and 437 deletions.
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

0 comments on commit 22eae52

Please sign in to comment.