diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f00fc3a..d250e0005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/static/schemas/piral-v0.json b/docs/static/schemas/piral-v0.json index 04bc7e2cc..4739eee23 100644 --- a/docs/static/schemas/piral-v0.json +++ b/docs/static/schemas/piral-v0.json @@ -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.", diff --git a/src/framework/piral-base/src/inspect.ts b/src/framework/piral-base/src/inspect.ts index b5b7615e4..6a5ff478d 100644 --- a/src/framework/piral-base/src/inspect.ts +++ b/src/framework/piral-base/src/inspect.ts @@ -5,6 +5,7 @@ import type { PiletV1Entry, PiletV2Entry, PiletV3Entry, + PiletMfEntry, PiletBundleEntry, PiletRunner, } from './types'; @@ -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]; @@ -26,6 +29,7 @@ export type InspectPiletResult = | InspectPiletV1 | InspectPiletV2 | InspectPiletV3 + | InspectPiletMf | InspectPiletUnknown | InspectPiletBundle; @@ -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') { diff --git a/src/framework/piral-base/src/loader.ts b/src/framework/piral-base/src/loader.ts index 62e92ee89..c470c1b9d 100644 --- a/src/framework/piral-base/src/loader.ts +++ b/src/framework/piral-base/src/loader.ts @@ -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'; @@ -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: diff --git a/src/framework/piral-base/src/loaders/empty/index.ts b/src/framework/piral-base/src/loaders/empty/index.ts index 3f04308de..4a6d48d18 100644 --- a/src/framework/piral-base/src/loaders/empty/index.ts +++ b/src/framework/piral-base/src/loaders/empty/index.ts @@ -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 { - const { name, spec = 'vx', dependencies = {}, ...rest } = entry; + const { name, spec = 'vx', ...rest } = entry; + const dependencies = 'dependencies' in entry ? entry.dependencies : {}; const meta = { name, version: '', @@ -21,6 +22,5 @@ export default function loader(entry: PiletEntry, _config: DefaultLoaderConfig): }; console.warn('Empty pilet found!', name); - return promisify({ ...meta, ...emptyApp }); } diff --git a/src/framework/piral-base/src/loaders/mf/index.ts b/src/framework/piral-base/src/loaders/mf/index.ts new file mode 100644 index 000000000..a4450ef46 --- /dev/null +++ b/src/framework/piral-base/src/loaders/mf/index.ts @@ -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; + }; + }; +} + +interface MfContainer { + init(scope: MfScope): void; + get(path: string): Promise; +} + +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 { + 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())); +} diff --git a/src/framework/piral-base/src/types/service.ts b/src/framework/piral-base/src/types/service.ts index f831966b5..fb51cf98a 100644 --- a/src/framework/piral-base/src/types/service.ts +++ b/src/framework/piral-base/src/types/service.ts @@ -197,6 +197,40 @@ export interface PiletV3Entry { dependencies?: Record; } +/** + * 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; +} + export interface PiletVxEntry { /** * The name of the pilet, i.e., the package id. @@ -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. diff --git a/src/samples/sample-piral/src/index.tsx b/src/samples/sample-piral/src/index.tsx index 37062a054..e28e96055 100644 --- a/src/samples/sample-piral/src/index.tsx +++ b/src/samples/sample-piral/src/index.tsx @@ -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);