Skip to content

Commit

Permalink
Implemented loading of module federation pilets
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianRappl committed Dec 1, 2023
1 parent 7aa3284 commit 2749c4e
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Added support for nested translations in `piral-translate` (#648)
- Added support for Angular 17 in `piral-ng`
- Added possibility to publish emulator as a website (#644)
- Added orchestration engine choices (`systemjs`, `module-federation`) in *piral.json* (#643)
- Added support for micro frontends based on module federation (#643)
- Added isolation mode option in *piral.json* to opt-in for `piral-component` boundary
- Added option to specify runtime execution path for bundler plugins

Expand Down
8 changes: 0 additions & 8 deletions docs/static/schemas/piral-v0.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@
],
"description": "Defines what isolation / component wrapper is used for components of micro frontends. By default, the 'classic' isolation mode is used."
},
"orchestrator": {
"type": "string",
"enum": [
"systemjs",
"module-federation"
],
"description": "Defines what orchestration engine is used by the app shell. By default, the 'systemjs' orchestration engine is used."
},
"pilets": {
"type": "object",
"description": "Determines the scaffolding and upgrading behavior of pilets using this Piral instance.",
Expand Down
6 changes: 6 additions & 0 deletions src/framework/piral-base/src/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
PiletV1Entry,
PiletV2Entry,
PiletV3Entry,
PiletMfEntry,
PiletBundleEntry,
PiletRunner,
} from './types';
Expand All @@ -17,6 +18,8 @@ export type InspectPiletV2 = ['v2', PiletV2Entry, PiletRunner];

export type InspectPiletV3 = ['v3', PiletV3Entry, PiletRunner];

export type InspectPiletMf = ['mf', PiletMfEntry, PiletRunner];

export type InspectPiletBundle = ['bundle', PiletBundleEntry, PiletRunner];

export type InspectPiletUnknown = ['unknown', PiletEntry, PiletRunner];
Expand All @@ -26,6 +29,7 @@ export type InspectPiletResult =
| InspectPiletV1
| InspectPiletV2
| InspectPiletV3
| InspectPiletMf
| InspectPiletUnknown
| InspectPiletBundle;

Expand All @@ -34,6 +38,8 @@ export function inspectPilet(meta: PiletEntry): InspectPiletResult {

if ('link' in meta && meta.spec === 'v3') {
return ['v3', meta, setupSinglePilet];
} else if (inBrowser && 'link' in meta && meta.spec === 'mf') {
return ['mf', meta, setupSinglePilet];
} else if (inBrowser && 'link' in meta && meta.spec === 'v2') {
return ['v2', meta, setupSinglePilet];
} else if (inBrowser && 'requireRef' in meta && meta.spec !== 'v2') {
Expand Down
3 changes: 3 additions & 0 deletions src/framework/piral-base/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import loadV0 from './loaders/v0';
import loadV1 from './loaders/v1';
import loadV2 from './loaders/v2';
import loadV3 from './loaders/v3';
import loadMf from './loaders/mf';
import { isfunc } from './utils';
import { inspectPilet } from './inspect';
import type { DefaultLoaderConfig, PiletLoader, CustomSpecLoaders } from './types';
Expand Down Expand Up @@ -50,6 +51,8 @@ export function getDefaultLoader(config: DefaultLoaderConfig = {}): PiletLoader
return loadV1(r[1], config);
case 'v0':
return loadV0(r[1], config);
case 'mf':
return loadMf(r[1], config);
case 'bundle':
return loadBundle(r[1], config);
default:
Expand Down
4 changes: 2 additions & 2 deletions src/framework/piral-base/src/loaders/empty/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type { DefaultLoaderConfig, PiletEntry, Pilet } from '../../types';
* @returns The evaluated pilet that can now be integrated.
*/
export default function loader(entry: PiletEntry, _config: DefaultLoaderConfig): Promise<Pilet> {
const { name, spec = 'vx', dependencies = {}, ...rest } = entry;
const { name, spec = 'vx', ...rest } = entry;
const dependencies = 'dependencies' in entry ? entry.dependencies : {};
const meta = {
name,
version: '',
Expand All @@ -21,6 +22,5 @@ export default function loader(entry: PiletEntry, _config: DefaultLoaderConfig):
};

console.warn('Empty pilet found!', name);

return promisify({ ...meta, ...emptyApp });
}
94 changes: 94 additions & 0 deletions src/framework/piral-base/src/loaders/mf/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createEvaluatedPilet, includeScriptDependency, registerModule } from '../../utils';
import type { DefaultLoaderConfig, PiletMfEntry, Pilet } from '../../types';

interface MfFactory {
(): any;
}

interface MfScope {
[depName: string]: {
[depVersion: string]: {
from: string;
eager: boolean;
loaded?: number;
get(): Promise<MfFactory>;
};
};
}

interface MfContainer {
init(scope: MfScope): void;
get(path: string): Promise<MfFactory>;
}

const appShell = 'piral';

function populateKnownDependencies(scope: MfScope) {
// SystemJS to MF
for (const [entry] of System.entries()) {
const index = entry.lastIndexOf('@');

if (index > 0 && !entry.match(/^https?:\/\//)) {
const entryName = entry.substring(0, index);
const entryVersion = entry.substring(index + 1);

if (!(entryName in scope)) {
scope[entryName] = {};
}

scope[entryName][entryVersion] = {
from: appShell,
eager: false,
get: () => System.import(entry).then((result) => () => result),
};
}
}
}

function extractSharedDependencies(scope: MfScope) {
// MF to SystemJS
for (const entryName of Object.keys(scope)) {
const entries = scope[entryName];

for (const entryVersion of Object.keys(entries)) {
const entry = entries[entryVersion];

if (entry.from !== appShell) {
registerModule(`${entryName}@${entryVersion}`, () => entry.get().then((factory) => factory()));
}
}
}
}

function loadMfFactory(piletName: string, exposedName: string) {
const varName = piletName.replace(/^@/, '').replace('/', '-').replace(/\-/g, '_');
const container: MfContainer = window[varName];
const scope: MfScope = {};
container.init(scope);
populateKnownDependencies(scope);
extractSharedDependencies(scope);
return container.get(exposedName);
}

/**
* Loads the provided SystemJS-powered pilet.
* @param entry The pilet's entry.
* @param _config The loader configuration.
* @returns The evaluated pilet that can now be integrated.
*/
export default function loader(entry: PiletMfEntry, _config: DefaultLoaderConfig): Promise<Pilet> {
const { config = {}, name, link, ...rest } = entry;
const dependencies = {};
const exposedName = rest.custom?.exposed || './pilet';
const meta = {
name,
dependencies,
config,
link,
...rest,
};

return includeScriptDependency(link)
.then(() => loadMfFactory(name, exposedName))
.then((factory) => createEvaluatedPilet(meta, factory()));
}
36 changes: 35 additions & 1 deletion src/framework/piral-base/src/types/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,40 @@ export interface PiletV3Entry {
dependencies?: Record<string, string>;
}

/**
* Metadata for pilets using the v2 schema.
*/
export interface PiletMfEntry {
/**
* The name of the pilet, i.e., the package id.
*/
name: string;
/**
* The version of the pilet. Should be semantically versioned.
*/
version: string;
/**
* Provides the version of the specification for this pilet.
*/
spec: 'mf';
/**
* The computed integrity of the pilet.
*/
integrity?: string;
/**
* The fallback link for retrieving the content of the pilet.
*/
link: string;
/**
* Optionally provides some custom metadata for the pilet.
*/
custom?: any;
/**
* Optionally provides some configuration to be used in the pilet.
*/
config?: Record<string, any>;
}

export interface PiletVxEntry {
/**
* The name of the pilet, i.e., the package id.
Expand Down Expand Up @@ -262,7 +296,7 @@ export interface PiletBundleEntry {
/**
* The metadata response for a single pilet.
*/
export type SinglePiletEntry = PiletV0Entry | PiletV1Entry | PiletV2Entry | PiletV3Entry | PiletVxEntry;
export type SinglePiletEntry = PiletV0Entry | PiletV1Entry | PiletV2Entry | PiletV3Entry | PiletMfEntry | PiletVxEntry;

/**
* The metadata response for a multi pilet.
Expand Down
6 changes: 6 additions & 0 deletions src/samples/sample-piral/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const instance = createInstance({
}),
],
requestPilets() {
return Promise.resolve([{
spec: 'mf',
name: '@wmf/foo',
version: '1.0.0',
link: 'http://localhost:8080/index.js',
}]);
return fetch('https://feed.piral.cloud/api/v1/pilet/sample')
.then((res) => res.json())
.then((res) => res.items);
Expand Down

0 comments on commit 2749c4e

Please sign in to comment.