-
Notifications
You must be signed in to change notification settings - Fork 156
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web-runtime): introduce skeleton app and runtime-hooks
in the proccess of adding a skeleton application we faced the issue of not having real runtime hooks. this introduces 3 hooks and exposes a dedicated runtime api to the application. this is fully backwards compatible.
- Loading branch information
Showing
18 changed files
with
920 additions
and
327 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
import VueRouter, { RouteConfig } from 'vue-router' | ||
import clone from 'lodash-es/clone' | ||
import { | ||
ClassicApplicationScript, | ||
RuntimeApi, | ||
ApplicationNavigationItem, | ||
ApplicationQuickActions | ||
} from './types' | ||
import { ApiError } from './error' | ||
import { get, isEqual, isObject, isArray } from 'lodash-es' | ||
import { Store } from 'vuex' | ||
import Vue, { Component } from 'vue' | ||
import { Wormhole } from 'portal-vue' | ||
|
||
/** | ||
* inject application specific routes into runtime | ||
* | ||
* @param applicationId | ||
* @param router | ||
* @param routes | ||
*/ | ||
const announceRoutes = (applicationId: string, router: VueRouter, routes: RouteConfig[]): void => { | ||
if (!isArray(routes)) { | ||
throw new ApiError("routes can't be blank") | ||
} | ||
|
||
const applicationRoutes = routes.map(applicationRoute => { | ||
if (!isObject(applicationRoute)) { | ||
throw new ApiError("route can't be blank", applicationRoute) | ||
} | ||
|
||
const route = clone(applicationRoute) | ||
route.name = applicationId === route.name ? route.name : `${applicationId}-${route.name}` | ||
route.path = `/${encodeURI(applicationId)}${route.path}` | ||
|
||
if (route.children) { | ||
route.children = route.children.map(childRoute => { | ||
if (!isObject(applicationRoute)) { | ||
throw new ApiError("route children can't be blank", applicationRoute, childRoute) | ||
} | ||
|
||
const route = clone(childRoute) | ||
route.name = `${applicationId}-${childRoute.name}` | ||
return route | ||
}) | ||
} | ||
|
||
return route | ||
}) | ||
|
||
router.addRoutes(applicationRoutes) | ||
} | ||
|
||
/** | ||
* inject application specific navigation items into runtime | ||
* | ||
* @param applicationId | ||
* @param store | ||
* @param navigationItems | ||
*/ | ||
const announceNavigationItems = ( | ||
applicationId: string, | ||
store: Store<unknown>, | ||
navigationItems: ClassicApplicationScript['navItems'] | ||
): void => { | ||
if (!isObject(navigationItems)) { | ||
throw new ApiError("navigationItems can't be blank") | ||
} | ||
|
||
store.commit('SET_NAV_ITEMS_FROM_CONFIG', { | ||
extension: applicationId, | ||
navItems: navigationItems | ||
}) | ||
} | ||
|
||
/** | ||
* inject application specific extension into runtime | ||
* | ||
* @param applicationId | ||
* @param store | ||
* @param extension | ||
*/ | ||
const announceExtension = ( | ||
applicationId: string, | ||
store: Store<unknown>, | ||
extension: { [key: string]: unknown } | ||
): void => { | ||
store.commit('REGISTER_EXTENSION', { app: applicationId, extension }) | ||
} | ||
|
||
/** | ||
* inject application specific translations into runtime | ||
* | ||
* @param translations | ||
* @param appTranslations | ||
* @param supportedLanguages | ||
*/ | ||
const announceTranslations = ( | ||
supportedLanguages: { [key: string]: string }, | ||
translations: unknown, | ||
appTranslations: ClassicApplicationScript['translations'] | ||
): void => { | ||
if (!isObject(translations)) { | ||
throw new ApiError("translations can't be blank") | ||
} | ||
|
||
Object.keys(supportedLanguages).forEach(lang => { | ||
if (translations[lang] && appTranslations[lang]) { | ||
Object.assign(translations[lang], appTranslations[lang]) | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* inject application specific quickActions into runtime | ||
* | ||
* @param store | ||
* @param quickActions | ||
*/ | ||
const announceQuickActions = ( | ||
store: Store<unknown>, | ||
quickActions: ClassicApplicationScript['quickActions'] | ||
): void => { | ||
if (!isObject(quickActions)) { | ||
throw new ApiError("quickActions can't be blank") | ||
} | ||
|
||
store.commit('ADD_QUICK_ACTIONS', quickActions) | ||
} | ||
|
||
/** | ||
* inject application specific store into runtime | ||
* | ||
* @param applicationName | ||
* @param store | ||
* @param applicationStore | ||
*/ | ||
const announceStore = ( | ||
applicationName: string, | ||
store: Store<unknown>, | ||
applicationStore: unknown | ||
): void => { | ||
const obtainedStore: Store<unknown> = get(applicationStore, 'default', applicationStore) | ||
|
||
if (!isObject(obtainedStore)) { | ||
throw new ApiError("store can't be blank") | ||
} | ||
|
||
store.registerModule(applicationName, obtainedStore) | ||
} | ||
|
||
/** | ||
* open a wormhole portal, this wraps vue-portal | ||
* | ||
* @param instance | ||
* @param applicationId | ||
* @param toApp | ||
* @param toPortal | ||
* @param order | ||
* @param components | ||
*/ | ||
const openPortal = ( | ||
applicationId: string, | ||
instance: typeof Vue.prototype, | ||
toApp: string, | ||
toPortal: string, | ||
order: number, | ||
components: Component[] | ||
): void => { | ||
Wormhole.open({ | ||
to: ['app', toApp, toPortal].filter(Boolean).join('.'), | ||
from: ['app', applicationId, toPortal, order].filter(Boolean).join('.'), | ||
order: order, | ||
passengers: components.map(instance.$createElement) | ||
}) | ||
} | ||
|
||
/** | ||
* expose store to the application | ||
* | ||
* @deprecated use with caution | ||
* | ||
* @param store | ||
*/ | ||
const requestStore = (store: Store<unknown>): Store<unknown> => { | ||
if (isEqual(process.env.NODE_ENV, 'development')) { | ||
console.warn('requestStore // store api is deprecated, use with caution') | ||
} | ||
|
||
return store | ||
} | ||
|
||
/** | ||
* expose router to the application | ||
* | ||
* @deprecated use with caution | ||
* | ||
* @param router | ||
*/ | ||
const requestRouter = (router: VueRouter): VueRouter => { | ||
if (isEqual(process.env.NODE_ENV, 'development')) { | ||
console.warn('requestRouter // router api is deprecated, use with caution') | ||
} | ||
|
||
return router | ||
} | ||
|
||
/** | ||
* exposed runtime api, this wraps all available api actions in a closure and provide application | ||
* specific data to the implementations. | ||
* | ||
* each application get it's own provisioned api! | ||
* | ||
* @param applicationName | ||
* @param applicationId | ||
* @param store | ||
* @param router | ||
* @param translations | ||
* @param supportedLanguages | ||
*/ | ||
export const buildRuntimeApi = ({ | ||
applicationName, | ||
applicationId, | ||
store, | ||
router, | ||
translations, | ||
supportedLanguages | ||
}: { | ||
applicationName: string | ||
applicationId: string | ||
store: Store<unknown> | ||
translations: unknown | ||
router: VueRouter | ||
supportedLanguages: { [key: string]: string } | ||
}): RuntimeApi => { | ||
if (!applicationName) { | ||
throw new ApiError("applicationName can't be blank") | ||
} | ||
|
||
if (!applicationId) { | ||
throw new ApiError("applicationId can't be blank") | ||
} | ||
|
||
return { | ||
announceRoutes: (routes: RouteConfig[]): void => announceRoutes(applicationId, router, routes), | ||
announceNavigationItems: (navigationItems: ApplicationNavigationItem[]): void => | ||
announceNavigationItems(applicationId, store, navigationItems), | ||
announceTranslations: (appTranslations: unknown): void => | ||
announceTranslations(supportedLanguages, translations, appTranslations), | ||
announceQuickActions: (quickActions: ApplicationQuickActions): void => | ||
announceQuickActions(store, quickActions), | ||
announceStore: (applicationStore: Store<unknown>): void => | ||
announceStore(applicationName, store, applicationStore), | ||
announceExtension: (extension: { [key: string]: unknown }): void => | ||
announceExtension(applicationId, store, extension), | ||
requestStore: (): Store<unknown> => requestStore(store), | ||
requestRouter: (): VueRouter => requestRouter(router), | ||
openPortal: ( | ||
instance: typeof Vue.prototype, | ||
toApp: string, | ||
toPortal: string, | ||
order: number, | ||
components: Component[] | ||
): void => openPortal(applicationId, instance, toApp, toPortal, order, components) | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
packages/web-runtime/src/container/application/classic.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { ClassicApplicationScript, RuntimeApi } from '../types' | ||
import { buildRuntimeApi } from '../api' | ||
import Vue from 'vue' | ||
import { isFunction, isObject } from 'lodash-es' | ||
import { NextApplication } from './next' | ||
import { Store } from 'vuex' | ||
import VueRouter from 'vue-router' | ||
import { RuntimeError } from '../error' | ||
|
||
/** | ||
* this wraps a classic application structure into a next application format. | ||
* it is fully backward compatible and will stay around as a fallback. | ||
*/ | ||
class ClassicApplication extends NextApplication { | ||
private readonly applicationScript: ClassicApplicationScript | ||
|
||
constructor(runtimeApi: RuntimeApi, applicationScript: ClassicApplicationScript) { | ||
super(runtimeApi) | ||
this.applicationScript = applicationScript | ||
} | ||
|
||
prepare(): Promise<void> { | ||
const { routes, navItems, translations, quickActions, store } = this.applicationScript | ||
|
||
routes && this.runtimeApi.announceRoutes(routes) | ||
navItems && this.runtimeApi.announceNavigationItems(navItems) | ||
translations && this.runtimeApi.announceTranslations(translations) | ||
quickActions && this.runtimeApi.announceQuickActions(quickActions) | ||
store && this.runtimeApi.announceStore(store) | ||
|
||
return Promise.resolve(undefined) | ||
} | ||
|
||
register(): Promise<void> { | ||
return Promise.resolve(undefined) | ||
} | ||
|
||
mounted(instance: Vue): Promise<void> { | ||
const { mounted: mountedHook } = this.applicationScript | ||
isFunction(mountedHook) && | ||
mountedHook({ | ||
portal: { | ||
open: (...args) => this.runtimeApi.openPortal.apply(instance, [instance, ...args]) | ||
}, | ||
store: this.runtimeApi.requestStore(), | ||
router: this.runtimeApi.requestRouter(), | ||
...this.runtimeApi | ||
}) | ||
|
||
return Promise.resolve(undefined) | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param applicationPath | ||
* @param store | ||
* @param router | ||
* @param translations | ||
* @param supportedLanguages | ||
*/ | ||
export const convertClassicApplication = async ({ | ||
applicationScript, | ||
store, | ||
router, | ||
translations, | ||
supportedLanguages | ||
}: { | ||
applicationScript: ClassicApplicationScript | ||
store: Store<unknown> | ||
router: VueRouter | ||
translations: unknown | ||
supportedLanguages: { [key: string]: string } | ||
}): Promise<NextApplication> => { | ||
const { appInfo } = applicationScript | ||
|
||
if (!isObject(appInfo)) { | ||
throw new RuntimeError("appInfo can't be blank") | ||
} | ||
|
||
const { id: applicationId, name: applicationName } = appInfo | ||
|
||
if (!applicationId) { | ||
throw new RuntimeError("appInfo.id can't be blank") | ||
} | ||
|
||
if (!applicationName) { | ||
throw new RuntimeError("appInfo.name can't be blank") | ||
} | ||
|
||
const runtimeApi = buildRuntimeApi({ | ||
applicationName, | ||
applicationId, | ||
store, | ||
router, | ||
translations, | ||
supportedLanguages | ||
}) | ||
|
||
await store.dispatch('registerApp', applicationScript.appInfo) | ||
|
||
return new ClassicApplication(runtimeApi, applicationScript) | ||
} |
Oops, something went wrong.