Skip to content

Commit

Permalink
feat(all): specify a Feature App's base URL (#476)
Browse files Browse the repository at this point in the history
This allows an integrator to specify a relative or absolute `baseUrl`
for two purposes:

1.) as a common base URL for relative `src`, `serverSrc`, and `css` hrefs
2.) to provide the Feature App with a base URL that it can use to
    reference its own resources
  • Loading branch information
unstubbable authored Apr 16, 2019
1 parent 3cd7e2c commit 5f05e7d
Show file tree
Hide file tree
Showing 21 changed files with 441 additions and 61 deletions.
77 changes: 74 additions & 3 deletions docs/guides/integrating-the-feature-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,13 @@ Additionally, when a Feature App needs to be rendered on the server, its
/>
```

> **Note:**
> If the integrator has configured the CommonJS module loader on the server, the
> Feature App to be loaded via `serverSrc` must be provided as a CommonJS module.
> **Notes:**
>
> - If the integrator has configured the CommonJS module loader on the server,
> the Feature App to be loaded via `serverSrc` must be provided as a CommonJS
> module.
> - If `baseUrl` is specified as well, it will be prepended if `serverSrc` is a
> relative URL. In this case `baseUrl` must be an absolute URL.
#### `css`

Expand All @@ -245,6 +249,28 @@ You can also define a `css` prop to add stylesheets to the document:
/>
```

#### `baseUrl`

Optionally, a relative or absolute `baseUrl` can be specified for two purposes:

1. as a common base URL for relative [`src`](#src), [`serverSrc`](#serversrc),
and [`css`](#css) hrefs
1. to provide the Feature App with a base URL with which it can refer to its own
resources

```jsx
<FeatureAppLoader
baseUrl="https://example.com/some-feature-app"
src="main.js"
serverSrc="main-node.js"
css={[{href: 'styles.css'}]}
/>
```

> **Note:**
> Only relative URLs are prepended with the `baseUrl`. Absolute URLs are used
> unchanged.
#### `idSpecifier`

If multiple instances of the same Feature App are placed on a single web page,
Expand Down Expand Up @@ -302,6 +328,18 @@ import {someFeatureAppDefinition} from './some-feature-app';
<FeatureAppContainer featureAppDefinition={someFeatureAppDefinition} />
```

#### `baseUrl`

Optionally, a `baseUrl` can be specified to provide the Feature App with a base
URL that it can use to reference its own resources:

```jsx
<FeatureAppLoader
featureAppDefinition={someFeatureAppDefinition}
baseUrl="https://example.com/some-feature-app"
/>
```

#### `idSpecifier`

If multiple instances of the same Feature App are placed on a single web page,
Expand Down Expand Up @@ -387,6 +425,21 @@ is the URL of its module bundle:
> If the integrator has configured the AMD module loader, the Feature App to be
> loaded via `src` must be provided as an [AMD module][amd].
#### `baseUrl`

Optionally, a `baseUrl` can be specified for two purposes:

1. as a common base URL for a relative [`src`](#src-1)
1. to provide the Feature App with a base URL with which it can refer to its own
resources

```html
<feature-app-loader
baseUrl="https://example.com/some-feature-app"
src="main.js"
></feature-app-loader>
```

#### `idSpecifier`

If multiple instances of the same Feature App are placed on a single web page,
Expand Down Expand Up @@ -468,6 +521,24 @@ featureAppContainer.featureAppDefinition = someFeatureAppDefinition;
document.querySelector('#app').appendChild(featureAppContainer);
```

#### `baseUrl`

Optionally, a `baseUrl` can be specified to provide the Feature App with a base
URL that it can use to reference its own resources:

```js
const featureAppContainer = document.createElement('feature-app-container');

featureAppContainer.featureAppDefinition = someFeatureAppDefinition;

featureAppContainer.setAttribute(
'baseUrl',
'https://example.com/some-feature-app'
);

document.querySelector('#app').appendChild(featureAppContainer);
```

#### `idSpecifier`

If multiple instances of the same Feature App are placed on a single web page,
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/reducing-the-bundle-size.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ two settings need to be made:
id: 'acme:my-feature-app',

create(env) {
__webpack_public_path__ = '/path/to/my-feature-app';
__webpack_public_path__ = env.baseUrl;

// ...
}
Expand All @@ -48,7 +48,7 @@ two settings need to be made:
id: 'acme:my-feature-service',

create(env) {
__webpack_public_path__ = '/path/to/my-feature-service';
__webpack_public_path__ = env.baseUrl;

// ...
}
Expand Down
9 changes: 9 additions & 0 deletions docs/guides/writing-a-feature-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ properties:
1. `idSpecifier` — An optional [ID specifier][idspecifier] that distinguishes
the Feature App instance from other Feature App instances with the same ID.

1. `baseUrl` — A base URL to be used for referencing the Feature App's own
resources. It is only set in the `env` if the integrator has defined a
`baseUrl` on the corresponding
[`FeatureAppLoader`][feature-app-loader-base-url] or
[`FeatureAppContainer`][feature-app-container-base-url].

The return value of the `create` method can vary depending on the integration
solution used. Assuming the [`@feature-hub/react`][react-api] package is used, a
Feature App can be either a [React Feature App][react-feature-app] or a [DOM
Expand Down Expand Up @@ -262,6 +268,9 @@ const myFeatureAppDefinition = {
[feature-service-binder]:
/docs/guides/writing-a-feature-service#feature-service-binder
[idspecifier]: /docs/guides/integrating-the-feature-hub#idspecifier
[feature-app-loader-base-url]: /docs/guides/integrating-the-feature-hub#baseurl
[feature-app-container-base-url]:
/docs/guides/integrating-the-feature-hub#baseurl-1
[placing-feature-apps-on-a-web-page-using-react]:
/docs/guides/integrating-the-feature-hub#placing-feature-apps-on-a-web-page-using-react
[placing-feature-apps-on-a-web-page-using-web-components]:
Expand Down
1 change: 1 addition & 0 deletions packages/async-ssr-manager/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('asyncSsrManagerDefinition', () => {

beforeEach(() => {
mockEnv = {
baseUrl: undefined,
config: {timeout: 5},
featureServices: {'s2:logger': stubbedLogger},
idSpecifier: undefined,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/__tests__/feature-app-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ describe('FeatureAppManager', () => {
const config = {kind: 'test'};
const idSpecifier = 'testIdSpecifier';
const instanceConfig = 'testInstanceConfig';
const baseUrl = '/base';

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

featureAppManager.getFeatureAppScope(mockFeatureAppDefinition, {
baseUrl,
idSpecifier,
instanceConfig
});
Expand All @@ -173,7 +175,7 @@ describe('FeatureAppManager', () => {
const {featureServices} = mockFeatureServicesBinding;

expect(mockFeatureAppCreate.mock.calls).toEqual([
[{config, instanceConfig, featureServices, idSpecifier}]
[{baseUrl, config, instanceConfig, featureServices, idSpecifier}]
]);
});

Expand Down
27 changes: 17 additions & 10 deletions packages/core/src/feature-app-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface FeatureAppEnvironment<
* other Feature App instances with the same ID.
*/
readonly idSpecifier: string | undefined;

/**
* The absolute or relative base URL of the Feature App's assets and/or BFF.
*/
readonly baseUrl: string | undefined;
}

export interface FeatureAppDefinition<
Expand Down Expand Up @@ -69,6 +74,11 @@ export interface FeatureAppConfigs {
}

export interface FeatureAppScopeOptions {
/**
* The absolute or relative base URL of the Feature App's assets and/or BFF.
*/
readonly baseUrl?: string;

/**
* A specifier to distinguish the Feature App instances from others created
* from the same definition.
Expand Down Expand Up @@ -201,7 +211,7 @@ export class FeatureAppManager {
featureAppDefinition: FeatureAppDefinition<TFeatureApp>,
options: FeatureAppScopeOptions = {}
): FeatureAppScope<TFeatureApp> {
const {idSpecifier, instanceConfig} = options;
const {baseUrl, idSpecifier, instanceConfig} = options;
const {id: featureAppId} = featureAppDefinition;
const featureAppUid = createUid(featureAppId, idSpecifier);

Expand All @@ -210,14 +220,10 @@ export class FeatureAppManager {
if (!featureAppScope) {
this.registerOwnFeatureServices(featureAppDefinition);

const deleteFeatureAppScope = () =>
this.featureAppScopes.delete(featureAppUid);

featureAppScope = this.createFeatureAppScope(
featureAppDefinition,
idSpecifier,
instanceConfig,
deleteFeatureAppScope
() => this.featureAppScopes.delete(featureAppUid),
{baseUrl, idSpecifier, instanceConfig}
);

this.featureAppScopes.set(featureAppUid, featureAppScope);
Expand Down Expand Up @@ -295,12 +301,12 @@ export class FeatureAppManager {

private createFeatureAppScope<TFeatureApp>(
featureAppDefinition: FeatureAppDefinition<TFeatureApp>,
idSpecifier: string | undefined,
instanceConfig: unknown,
deleteFeatureAppScope: () => void
deleteFeatureAppScope: () => void,
options: FeatureAppScopeOptions
): FeatureAppScope<TFeatureApp> {
this.validateExternals(featureAppDefinition);

const {baseUrl, idSpecifier, instanceConfig} = options;
const {configs} = this.options;
const config = configs && configs[featureAppDefinition.id];
const featureAppUid = createUid(featureAppDefinition.id, idSpecifier);
Expand All @@ -311,6 +317,7 @@ export class FeatureAppManager {
);

const featureApp = featureAppDefinition.create({
baseUrl,
config,
instanceConfig,
featureServices: binding.featureServices,
Expand Down
9 changes: 5 additions & 4 deletions packages/demos/src/todomvc/integrator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ ReactDOM.render(
<FeatureHubContextProvider value={{featureAppManager}}>
<section className="todoapp">
<FeatureAppLoader
src="header/feature-app-header.umd.js"
css={[{href: 'header/index.css'}]}
baseUrl="header"
src="feature-app-header.umd.js"
css={[{href: 'index.css'}]}
/>
<FeatureAppLoader src="main/feature-app-main.umd.js" />
<FeatureAppLoader src="footer/feature-app-footer.umd.js" />
<FeatureAppLoader baseUrl="main" src="feature-app-main.umd.js" />
<FeatureAppLoader baseUrl="footer" src="feature-app-footer.umd.js" />
</section>
</FeatureHubContextProvider>,
document.querySelector('main')
Expand Down
6 changes: 5 additions & 1 deletion packages/dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
"dependencies": {
"@feature-hub/core": "^1.4.0",
"lit-element": "^2.0.1",
"lit-html": "^1.0.0"
"lit-html": "^1.0.0",
"url-join": "^4.0.0"
},
"devDependencies": {
"@types/url-join": "^4.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
14 changes: 13 additions & 1 deletion packages/dom/src/feature-app-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface DomFeatureApp {
* Feature App throws in its [[DomFeatureApp.attachTo]] method.
*/
export interface FeatureAppContainerElement extends HTMLElement {
/**
* The absolute or relative base URL of the Feature App's assets and/or BFF.
*/
baseUrl?: string;

/**
* The definition of the Feature App that should be rendered.
*/
Expand Down Expand Up @@ -73,6 +78,9 @@ export function defineFeatureAppContainer(

class FeatureAppContainer extends LitElement
implements FeatureAppContainerElement {
@property({type: String})
public baseUrl?: string;

@property({type: Object})
public featureAppDefinition?: FeatureAppDefinition<DomFeatureApp>;

Expand All @@ -97,7 +105,11 @@ export function defineFeatureAppContainer(
try {
this.featureAppScope = featureAppManager.getFeatureAppScope(
this.featureAppDefinition,
{idSpecifier: this.idSpecifier, instanceConfig: this.instanceConfig}
{
baseUrl: this.baseUrl,
idSpecifier: this.idSpecifier,
instanceConfig: this.instanceConfig
}
);

this.featureAppScope.featureApp.attachTo(this.appElement);
Expand Down
15 changes: 13 additions & 2 deletions packages/dom/src/feature-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {TemplateResult} from 'lit-html';
import {ifDefined} from 'lit-html/directives/if-defined';
import {until} from 'lit-html/directives/until';
import {defineFeatureAppContainer} from './feature-app-container';
import {prependBaseUrl} from './internal/prepend-base-url';

const elementName = 'feature-app-loader';

Expand All @@ -19,7 +20,13 @@ const elementName = 'feature-app-loader';
*/
export interface FeatureAppLoaderElement extends HTMLElement {
/**
* A URL pointing to a Feature App's module bundle.
* The absolute or relative base URL of the Feature App's assets and/or BFF.
*/
baseUrl?: string;

/**
* The URL of the Feature App's module bundle. If [[baseUrl]] is specified, it
* will be prepended, unless `src` is an absolute URL.
*/
src: string;

Expand Down Expand Up @@ -62,6 +69,9 @@ export function defineFeatureAppLoader(
defineFeatureAppContainer(featureAppManager, options);

class FeatureAppLoader extends LitElement implements FeatureAppLoaderElement {
@property({type: String})
public baseUrl?: string;

@property({type: String})
public src!: string;

Expand Down Expand Up @@ -89,11 +99,12 @@ export function defineFeatureAppLoader(
}

const definition = await featureAppManager.getAsyncFeatureAppDefinition(
this.src
prependBaseUrl(this.baseUrl, this.src)
).promise;

return html`
<feature-app-container
baseUrl=${ifDefined(this.baseUrl)}
idSpecifier=${ifDefined(this.idSpecifier)}
.featureAppDefinition=${definition}
.instanceConfig=${this.instanceConfig}
Expand Down
17 changes: 17 additions & 0 deletions packages/dom/src/internal/prepend-base-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import urljoin from 'url-join';

function isAbsolute(url: string): boolean {
return /^https?:\/\//.test(url);
}

/**
* Prepends the given `base` - which can be relative or absolute - to the `url`,
* unless the `url` is absolute.
*/
export function prependBaseUrl(base: string | undefined, url: string): string {
if (!base || isAbsolute(url)) {
return url;
}

return urljoin(base, url);
}
2 changes: 1 addition & 1 deletion packages/react/.size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
module.exports = [
{
path: 'lib/cjs/index.js',
limit: '2 KB'
limit: '3 KB'
}
];
Loading

0 comments on commit 5f05e7d

Please sign in to comment.