Skip to content

Commit

Permalink
fix(module-federation): Throw an error if remote is invalid
Browse files Browse the repository at this point in the history
closes: #23024
  • Loading branch information
ndcunningham committed May 3, 2024
1 parent e15720b commit 262d90c
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 1 deletion.
11 changes: 11 additions & 0 deletions e2e/angular/src/module-federation.test.ts
Expand Up @@ -310,6 +310,17 @@ describe('Angular Module Federation', () => {
await killProcessAndPorts(processTsNode.pid, hostPort, remotePort);
}, 20_000_000);

it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
const shell = uniq('shell');
const remote = uniq('remote-1');
const result = runCLI(
`generate @nx/angular:host ${shell} --remotes=${remote} --e2eTestRunner=none --dynamic=true --project-name-and-root-format=as-provided --no-interactive --skipFormat`,
{ silenceError: true }
);

expect(result).toContain(`Invalid remote name provided: ${remote}`);
});

it('should federate a module from a library and update an existing remote', async () => {
const lib = uniq('lib');
const remote = uniq('remote');
Expand Down
11 changes: 11 additions & 0 deletions e2e/react/src/react-module-federation.test.ts
Expand Up @@ -1001,6 +1001,17 @@ describe('React Module Federation', () => {
}
}, 500_000);
});

it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
const shell = uniq('shell');
const remote = uniq('remote-1');
const result = runCLI(
`generate @nx/react:host ${shell} --remotes=${remote} --e2eTestRunner=none --dynamic=true --project-name-and-root-format=as-provided --no-interactive --skipFormat`,
{ silenceError: true }
);

expect(result).toContain(`Invalid remote name provided: ${remote}`);
}, 200_000);
});

function readPort(appName: string): number {
Expand Down
2 changes: 1 addition & 1 deletion e2e/utils/command-utils.ts
Expand Up @@ -376,7 +376,7 @@ export function runCLI(
return r;
} catch (e) {
if (opts.silenceError) {
return stripConsoleColors(e.stdout + e.stderr);
return stripConsoleColors(e.stdout?.toString() + e.stderr?.toString());
} else {
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
throw e;
Expand Down
14 changes: 14 additions & 0 deletions packages/angular/src/generators/host/host.ts
Expand Up @@ -13,6 +13,7 @@ import { setupMf } from '../setup-mf/setup-mf';
import { updateSsrSetup } from './lib';
import type { Schema } from './schema';
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
import { isValidVariable } from '@nx/js';

export async function host(tree: Tree, options: Schema) {
return await hostInternal(tree, {
Expand All @@ -30,6 +31,19 @@ export async function hostInternal(tree: Tree, schema: Schema) {
const remotesToGenerate: string[] = [];
const remotesToIntegrate: string[] = [];

// Check to see if remotes are provided and also check if --dynamic is provided
// if both are check that the remotes are valid names else throw an error.
if (options.dynamic && options.remotes?.length > 0) {
options.remotes.forEach((remote) => {
const isValidRemote = isValidVariable(remote);
if (!isValidRemote.isValid) {
throw new Error(
`Invalid remote name provided: ${remote}. ${isValidRemote.message}`
);
}
});
}

if (options.remotes && options.remotes.length > 0) {
options.remotes.forEach((remote) => {
if (!projects.has(remote)) {
Expand Down
1 change: 1 addition & 0 deletions packages/js/src/index.ts
Expand Up @@ -13,6 +13,7 @@ export * from './utils/package-json/update-package-json';
export * from './utils/package-json/create-entry-points';
export { libraryGenerator } from './generators/library/library';
export { initGenerator } from './generators/init/init';
export { isValidVariable } from './utils/is-valid-variable';

// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export {
Expand Down
37 changes: 37 additions & 0 deletions packages/js/src/utils/is-valid-variable.ts
@@ -0,0 +1,37 @@
/**
* Determines if a given string is a valid JavaScript variable name.
* @param name name of the variable to be checked
* @returns result object with a boolean indicating if the name is valid and a message explaining why it is not valid
*/
export function isValidVariable(name: string): {
isValid: boolean;
message: string;
} {
const validRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;

if (validRegex.test(name)) {
return { isValid: true, message: 'The name is a valid identifier.' };
} else {
if (name === '') {
return { isValid: false, message: 'The name cannot be empty.' };
} else if (/^[0-9]/.test(name)) {
return { isValid: false, message: 'The name cannot start with a digit.' };
} else if (/[^a-zA-Z0-9_$]/.test(name)) {
return {
isValid: false,
message:
'The name can only contain letters, digits, underscores, and dollar signs.',
};
} else if (/^[^a-zA-Z_$]/.test(name)) {
return {
isValid: false,
message:
'The name must start with a letter, underscore, or dollar sign.',
};
}
return {
isValid: false,
message: 'The name is not a valid JavaScript identifier.',
};
}
}
14 changes: 14 additions & 0 deletions packages/react/src/generators/host/host.ts
Expand Up @@ -21,6 +21,7 @@ import { setupSsrForHost } from './lib/setup-ssr-for-host';
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
import { NormalizedSchema, Schema } from './schema';
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
import { isValidVariable } from '@nx/js';

export async function hostGenerator(
host: Tree,
Expand Down Expand Up @@ -48,6 +49,19 @@ export async function hostGeneratorInternal(
addPlugin: false,
};

// Check to see if remotes are provided and also check if --dynamic is provided
// if both are check that the remotes are valid names else throw an error.
if (options.dynamic && options.remotes?.length > 0) {
options.remotes.forEach((remote) => {
const isValidRemote = isValidVariable(remote);
if (!isValidRemote.isValid) {
throw new Error(
`Invalid remote name provided: ${remote}. ${isValidRemote.message}`
);
}
});
}

const initTask = await applicationGenerator(host, {
...options,
// The target use-case is loading remotes as child routes, thus always enable routing.
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/generators/remote/remote.ts
Expand Up @@ -23,6 +23,7 @@ import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
import { maybeJs } from '../../utils/maybe-js';
import { isValidVariable } from '@nx/js';

export function addModuleFederationFiles(
host: Tree,
Expand Down Expand Up @@ -90,6 +91,18 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
// TODO(colum): remove when MF works with Crystal
addPlugin: false,
};

if (options.dynamic) {
// Dynamic remotes generate with library { type: 'var' } by default.
// We need to ensure that the remote name is a valid variable name.
const isValidRemote = isValidVariable(options.name);
if (!isValidRemote.isValid) {
throw new Error(
`Invalid remote name provided: ${options.name}. ${isValidRemote.message}`
);
}
}

const initAppTask = await applicationGenerator(host, {
...options,
// Only webpack works with module federation for now.
Expand Down

0 comments on commit 262d90c

Please sign in to comment.