fix(module-federation): Throw an error if remote is invalid (#23100)
If you are generating a remote using `--dynamic` either by using the `host` generator or the `remote` generator we now check to ensure that the remote name is a valid JavaScript variable. If this is not done the app with be invalid and unable to be ran or bundled. closes: #23024
This commit is contained in:
parent
bdac1e2a6f
commit
0322b9804f
@ -6,11 +6,12 @@ import {
|
|||||||
getProjects,
|
getProjects,
|
||||||
readProjectConfiguration,
|
readProjectConfiguration,
|
||||||
} from 'nx/src/generators/utils/project-configuration';
|
} from 'nx/src/generators/utils/project-configuration';
|
||||||
import { E2eTestRunner } from '../../utils/test-runners';
|
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
|
||||||
import {
|
import {
|
||||||
generateTestHostApplication,
|
generateTestHostApplication,
|
||||||
generateTestRemoteApplication,
|
generateTestRemoteApplication,
|
||||||
} from '../utils/testing';
|
} from '../utils/testing';
|
||||||
|
import { Linter } from '@nx/eslint';
|
||||||
|
|
||||||
describe('Host App Generator', () => {
|
describe('Host App Generator', () => {
|
||||||
it('should generate a host app with no remotes', async () => {
|
it('should generate a host app with no remotes', async () => {
|
||||||
@ -632,4 +633,23 @@ describe('Host App Generator', () => {
|
|||||||
const packageJson = readJson(tree, 'package.json');
|
const packageJson = readJson(tree, 'package.json');
|
||||||
expect(packageJson).toEqual(initialPackageJson);
|
expect(packageJson).toEqual(initialPackageJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
const remote = 'invalid-remote-name';
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
generateTestHostApplication(tree, {
|
||||||
|
name: 'myhostapp',
|
||||||
|
remotes: [remote],
|
||||||
|
dynamic: true,
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
e2eTestRunner: E2eTestRunner.None,
|
||||||
|
linter: Linter.None,
|
||||||
|
style: 'css',
|
||||||
|
unitTestRunner: UnitTestRunner.None,
|
||||||
|
typescriptConfiguration: false,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(`Invalid remote name provided: ${remote}.`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { setupMf } from '../setup-mf/setup-mf';
|
|||||||
import { updateSsrSetup } from './lib';
|
import { updateSsrSetup } from './lib';
|
||||||
import type { Schema } from './schema';
|
import type { Schema } from './schema';
|
||||||
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
||||||
|
import { isValidVariable } from '@nx/js';
|
||||||
|
|
||||||
export async function host(tree: Tree, options: Schema) {
|
export async function host(tree: Tree, options: Schema) {
|
||||||
return await hostInternal(tree, {
|
return await hostInternal(tree, {
|
||||||
@ -30,6 +31,19 @@ export async function hostInternal(tree: Tree, schema: Schema) {
|
|||||||
const remotesToGenerate: string[] = [];
|
const remotesToGenerate: string[] = [];
|
||||||
const remotesToIntegrate: 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) {
|
if (options.remotes && options.remotes.length > 0) {
|
||||||
options.remotes.forEach((remote) => {
|
options.remotes.forEach((remote) => {
|
||||||
if (!projects.has(remote)) {
|
if (!projects.has(remote)) {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export * from './utils/package-json/update-package-json';
|
|||||||
export * from './utils/package-json/create-entry-points';
|
export * from './utils/package-json/create-entry-points';
|
||||||
export { libraryGenerator } from './generators/library/library';
|
export { libraryGenerator } from './generators/library/library';
|
||||||
export { initGenerator } from './generators/init/init';
|
export { initGenerator } from './generators/init/init';
|
||||||
|
export { isValidVariable } from './utils/is-valid-variable';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
export {
|
export {
|
||||||
|
|||||||
37
packages/js/src/utils/is-valid-variable.ts
Normal file
37
packages/js/src/utils/is-valid-variable.ts
Normal file
@ -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.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -340,4 +340,23 @@ describe('hostGenerator', () => {
|
|||||||
tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
|
tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
|
||||||
).toContain(`'remote1', 'remote2', 'remote3'`);
|
).toContain(`'remote1', 'remote2', 'remote3'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
const remote = 'invalid-remote-name';
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
hostGenerator(tree, {
|
||||||
|
name: 'myhostapp',
|
||||||
|
remotes: [remote],
|
||||||
|
dynamic: true,
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
e2eTestRunner: 'none',
|
||||||
|
linter: Linter.None,
|
||||||
|
style: 'css',
|
||||||
|
unitTestRunner: 'none',
|
||||||
|
typescriptConfiguration: false,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(`Invalid remote name provided: ${remote}.`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { setupSsrForHost } from './lib/setup-ssr-for-host';
|
|||||||
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
|
import { updateModuleFederationE2eProject } from './lib/update-module-federation-e2e-project';
|
||||||
import { NormalizedSchema, Schema } from './schema';
|
import { NormalizedSchema, Schema } from './schema';
|
||||||
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
|
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
|
||||||
|
import { isValidVariable } from '@nx/js';
|
||||||
|
|
||||||
export async function hostGenerator(
|
export async function hostGenerator(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -48,6 +49,19 @@ export async function hostGeneratorInternal(
|
|||||||
addPlugin: false,
|
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, {
|
const initTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
// The target use-case is loading remotes as child routes, thus always enable routing.
|
// The target use-case is loading remotes as child routes, thus always enable routing.
|
||||||
|
|||||||
@ -292,4 +292,24 @@ describe('remote generator', () => {
|
|||||||
tree.read('test/module-federation.server.config.ts', 'utf-8')
|
tree.read('test/module-federation.server.config.ts', 'utf-8')
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
const name = 'invalid-dynamic-remote-name';
|
||||||
|
await expect(
|
||||||
|
remote(tree, {
|
||||||
|
name,
|
||||||
|
devServerPort: 4209,
|
||||||
|
dynamic: true,
|
||||||
|
e2eTestRunner: 'cypress',
|
||||||
|
linter: Linter.EsLint,
|
||||||
|
skipFormat: false,
|
||||||
|
style: 'css',
|
||||||
|
unitTestRunner: 'jest',
|
||||||
|
ssr: true,
|
||||||
|
projectNameAndRootFormat: 'as-provided',
|
||||||
|
typescriptConfiguration: true,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError(`Invalid remote name provided: ${name}.`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
|
|||||||
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
|
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
|
||||||
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
|
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
|
||||||
import { maybeJs } from '../../utils/maybe-js';
|
import { maybeJs } from '../../utils/maybe-js';
|
||||||
|
import { isValidVariable } from '@nx/js';
|
||||||
|
|
||||||
export function addModuleFederationFiles(
|
export function addModuleFederationFiles(
|
||||||
host: Tree,
|
host: Tree,
|
||||||
@ -90,6 +91,18 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
|
|||||||
// TODO(colum): remove when MF works with Crystal
|
// TODO(colum): remove when MF works with Crystal
|
||||||
addPlugin: false,
|
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, {
|
const initAppTask = await applicationGenerator(host, {
|
||||||
...options,
|
...options,
|
||||||
// Only webpack works with module federation for now.
|
// Only webpack works with module federation for now.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user