Skip to content

Commit

Permalink
feat(core): make ExternalsValidator optional for the FeatureAppManager (
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable authored Feb 7, 2019
1 parent 3f0278d commit 18fba0d
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 123 deletions.
182 changes: 99 additions & 83 deletions packages/core/src/__tests__/feature-app-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ describe('FeatureAppManager', () => {
mockModuleLoader = jest.fn(async () => mockFeatureAppModule);
mockExternalsValidator = {validate: jest.fn()};

featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry);
});

afterEach(() => {
Expand All @@ -64,11 +61,9 @@ describe('FeatureAppManager', () => {

describe('#getAsyncFeatureAppDefinition', () => {
beforeEach(() => {
featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator,
{moduleLoader: mockModuleLoader}
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry, {
moduleLoader: mockModuleLoader
});
});

it('logs an info message when the Feature App module was loaded', async () => {
Expand Down Expand Up @@ -134,10 +129,7 @@ describe('FeatureAppManager', () => {
});

it('throws an error if no module loader was provided', () => {
featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry);

expect(() =>
featureAppManager.getAsyncFeatureAppDefinition('/example.js')
Expand All @@ -163,11 +155,9 @@ describe('FeatureAppManager', () => {
const mockConfig = {kind: 'test'};
const idSpecifier = 'testIdSpecifier';

featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator,
{configs: {[mockFeatureAppDefinition.id]: mockConfig}}
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry, {
configs: {[mockFeatureAppDefinition.id]: mockConfig}
});

featureAppManager.getFeatureAppScope(
mockFeatureAppDefinition,
Expand All @@ -185,79 +175,110 @@ describe('FeatureAppManager', () => {
]);
});

describe('with a Feature App definition that is failing the externals validation', () => {
let mockError: Error;
describe('without an ExternalsValidator provided to the FeatureAppManager', () => {
describe('with a Feature App definition that is declaring external dependencies', () => {
beforeEach(() => {
mockFeatureAppDefinition = {
...mockFeatureAppDefinition,
dependencies: {
externals: {
react: '^16.0.0'
}
}
};
});

beforeEach(() => {
mockError = new Error('mockError');
it("doesn't throw an error", () => {
expect(() => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);
}).not.toThrowError();
});
});
});

mockExternalsValidator.validate = jest.fn(() => {
throw mockError;
describe('with an ExternalsValidator provided to the FeatureAppManager', () => {
beforeEach(() => {
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry, {
externalsValidator: mockExternalsValidator
});
});

mockFeatureAppDefinition = {
...mockFeatureAppDefinition,
dependencies: {
externals: {
react: '^16.0.0'
describe('with a Feature App definition that is failing the externals validation', () => {
let mockError: Error;

beforeEach(() => {
mockError = new Error('mockError');

mockExternalsValidator.validate = jest.fn(() => {
throw mockError;
});

mockFeatureAppDefinition = {
...mockFeatureAppDefinition,
dependencies: {
externals: {
react: '^16.0.0'
}
}
}
};
});
};
});

it('calls the provided ExternalsValidator with the defined externals', () => {
try {
featureAppManager.getFeatureAppScope(
mockFeatureAppDefinition,
'testIdSpecifier'
);
} catch {}
it('calls the provided ExternalsValidator with the defined externals', () => {
try {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);
} catch {}

expect(mockExternalsValidator.validate).toHaveBeenCalledWith({
react: '^16.0.0'
expect(mockExternalsValidator.validate).toHaveBeenCalledWith({
react: '^16.0.0'
});
});
});

it('throws the validation error', () => {
expect(() => {
featureAppManager.getFeatureAppScope(
mockFeatureAppDefinition,
'testIdSpecifier'
);
}).toThrowError(mockError);
it('throws the validation error', () => {
expect(() => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);
}).toThrowError(mockError);
});
});
});

describe('with a Feature App definition that is not failing the externals validation', () => {
beforeEach(() => {
mockFeatureAppDefinition = {
...mockFeatureAppDefinition,
dependencies: {
externals: {
react: '^16.0.0'
describe('with a Feature App definition that is not failing the externals validation', () => {
beforeEach(() => {
mockFeatureAppDefinition = {
...mockFeatureAppDefinition,
dependencies: {
externals: {
react: '^16.0.0'
}
}
}
};
});
};
});

it('calls the provided ExternalsValidator with the defined externals', () => {
featureAppManager.getFeatureAppScope(
mockFeatureAppDefinition,
'testIdSpecifier'
);
it('calls the provided ExternalsValidator with the defined externals', () => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);

expect(mockExternalsValidator.validate).toHaveBeenCalledWith({
react: '^16.0.0'
});
});

expect(mockExternalsValidator.validate).toHaveBeenCalledWith({
react: '^16.0.0'
it("doesn't throw an error", () => {
expect(() => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);
}).not.toThrowError();
});
});

it("doesn't throw an error", () => {
expect(() => {
featureAppManager.getFeatureAppScope(
mockFeatureAppDefinition,
'testIdSpecifier'
);
}).not.toThrowError();
describe('with a Feature App definition that declares no externals', () => {
it('does not call the provided ExternalsValidator', () => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);

expect(mockExternalsValidator.validate).not.toHaveBeenCalled();
});

it("doesn't throw an error", () => {
expect(() => {
featureAppManager.getFeatureAppScope(mockFeatureAppDefinition);
}).not.toThrowError();
});
});
});

Expand Down Expand Up @@ -487,11 +508,9 @@ describe('FeatureAppManager', () => {

describe('#preloadFeatureApp', () => {
beforeEach(() => {
featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator,
{moduleLoader: mockModuleLoader}
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry, {
moduleLoader: mockModuleLoader
});
});

it('preloads a Feature App definition so that the scope is synchronously available', async () => {
Expand All @@ -505,10 +524,7 @@ describe('FeatureAppManager', () => {
});

it('throws an error if no module loader was provided', () => {
featureAppManager = new FeatureAppManager(
mockFeatureServiceRegistry,
mockExternalsValidator
);
featureAppManager = new FeatureAppManager(mockFeatureServiceRegistry);

expect(() =>
featureAppManager.getAsyncFeatureAppDefinition('/example.js')
Expand Down
29 changes: 27 additions & 2 deletions packages/core/src/feature-app-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,28 @@ export interface FeatureAppManagerLike {
}

export interface FeatureAppManagerOptions {
/**
* Configurations for all Feature Apps that will potentially be created.
*/
readonly configs?: FeatureAppConfigs;

/**
* For the `FeatureAppManager` to be able to load Feature Apps from a remote
* location, a module loader must be provided, (e.g. the
* `@feature-hub/module-loader-amd` package or the
* `@feature-hub/module-loader-commonjs` package).
*/
readonly moduleLoader?: ModuleLoader;

/**
* When using a {@link #moduleLoader}, it might make sense to validate
* external dependencies that are required by Feature Apps against the
* shared dependencies that are provided by the integrator. This makes it
* possible that an error is already thrown when creating a Feature App with
* incompatible external dependencies, and thus enables early feedback as to
* whether a Feature App is compatible with the integration environment.
*/
readonly externalsValidator?: ExternalsValidatorLike;
}

type FeatureAppModuleUrl = string;
Expand All @@ -97,7 +117,6 @@ export class FeatureAppManager implements FeatureAppManagerLike {

public constructor(
private readonly featureServiceRegistry: FeatureServiceRegistryLike,
private readonly externalsValidator: ExternalsValidatorLike,
private readonly options: FeatureAppManagerOptions = {}
) {}

Expand Down Expand Up @@ -299,10 +318,16 @@ export class FeatureAppManager implements FeatureAppManagerLike {
private validateExternals(
featureAppDefinition: FeatureServiceConsumerDefinition
): void {
const {externalsValidator} = this.options;

if (!externalsValidator) {
return;
}

const {dependencies} = featureAppDefinition;

if (dependencies && dependencies.externals) {
this.externalsValidator.validate(dependencies.externals);
externalsValidator.validate(dependencies.externals);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ export default async function renderApp({
const featureAppNodeUrl = `http://localhost:${port}/feature-app.commonjs.js`;
const featureServiceRegistry = new FeatureServiceRegistry(externalsValidator);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator,
{moduleLoader: loadCommonJsModule}
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry, {
moduleLoader: loadCommonJsModule,
externalsValidator
});

// In a real-world integrator, instead of preloading a Feature App manually
// before rendering, the Async SSR Manager would be used to handle the
Expand Down
9 changes: 4 additions & 5 deletions packages/demos/src/feature-app-in-feature-app/integrator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ const externalsValidator = new ExternalsValidator({

const featureServiceRegistry = new FeatureServiceRegistry(externalsValidator);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator,
{moduleLoader: loadAmdModule}
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry, {
moduleLoader: loadAmdModule,
externalsValidator
});

const {FeatureHubContextProvider, FeatureAppLoader} = FeatureHubReact;

Expand Down
5 changes: 1 addition & 4 deletions packages/demos/src/history-service/integrator.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ export default async function renderApp({
'test:integrator'
);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry);

const html = ReactDOM.renderToString(
<App featureAppManager={featureAppManager} />
Expand Down
5 changes: 1 addition & 4 deletions packages/demos/src/history-service/integrator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ featureServiceRegistry.registerFeatureServices(
'test:integrator'
);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry);

ReactDOM.render(
<App featureAppManager={featureAppManager} />,
Expand Down
9 changes: 4 additions & 5 deletions packages/demos/src/module-loader-amd/integrator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ const externalsValidator = new ExternalsValidator({

const featureServiceRegistry = new FeatureServiceRegistry(externalsValidator);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator,
{moduleLoader: loadAmdModule}
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry, {
moduleLoader: loadAmdModule,
externalsValidator
});

ReactDOM.render(
<FeatureHubContextProvider value={{featureAppManager}}>
Expand Down
8 changes: 3 additions & 5 deletions packages/demos/src/module-loader-commonjs/integrator.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ export default async function renderApp({
const externalsValidator = new ExternalsValidator({});
const featureServiceRegistry = new FeatureServiceRegistry(externalsValidator);

const featureAppManager = new FeatureAppManager(
featureServiceRegistry,
externalsValidator,
{moduleLoader: loadCommonJsModule}
);
const featureAppManager = new FeatureAppManager(featureServiceRegistry, {
moduleLoader: loadCommonJsModule
});

// In a real-world integrator, instead of preloading a Feature App manually
// before rendering, the Async SSR Manager would be used to handle the
Expand Down
Loading

0 comments on commit 18fba0d

Please sign in to comment.