Skip to content

Commit 782a5a7

Browse files
committed
chore: move scalprum to separate file
1 parent 4d2e5a4 commit 782a5a7

File tree

3 files changed

+397
-394
lines changed

3 files changed

+397
-394
lines changed

packages/core/src/index.ts

Lines changed: 3 additions & 393 deletions
Original file line numberDiff line numberDiff line change
@@ -1,393 +1,3 @@
1-
import { PluginStore, FeatureFlags, PluginLoaderOptions, PluginStoreOptions, PluginManifest } from '@openshift/dynamic-plugin-sdk';
2-
import { warnDuplicatePkg } from './warnDuplicatePkg';
3-
export const GLOBAL_NAMESPACE = '__scalprum__';
4-
export type AppMetadata<T extends {} = {}> = T & {
5-
name: string;
6-
appId?: string;
7-
elementId?: string;
8-
rootLocation?: string;
9-
scriptLocation?: string;
10-
manifestLocation?: string;
11-
pluginManifest?: PluginManifest;
12-
};
13-
export interface AppsConfig<T extends {} = {}> {
14-
[key: string]: AppMetadata<T>;
15-
}
16-
17-
export type PrefetchFunction<T = any> = (ScalprumApi: Record<string, any> | undefined) => Promise<T>;
18-
export type ExposedScalprumModule<T = any, P = any> = { [importName: string]: T } & { prefetch?: PrefetchFunction<P> };
19-
export type ScalprumModule<T = any, P = any> = {
20-
cachedModule?: ExposedScalprumModule<T, P>;
21-
prefetchPromise?: ReturnType<PrefetchFunction>;
22-
};
23-
24-
export interface Factory<T = any, P = any> {
25-
init: (sharing: any) => void;
26-
modules: {
27-
[key: string]: ExposedScalprumModule<T, P>;
28-
};
29-
expiration: Date;
30-
}
31-
32-
export interface ScalprumOptions {
33-
cacheTimeout: number;
34-
enableScopeWarning: boolean;
35-
}
36-
37-
export type Scalprum<T extends Record<string, any> = Record<string, any>> = {
38-
appsConfig: AppsConfig;
39-
pendingInjections: {
40-
[key: string]: Promise<any>;
41-
};
42-
pendingLoading: {
43-
[key: string]: Promise<ScalprumModule>;
44-
};
45-
pendingPrefetch: {
46-
[key: string]: Promise<unknown>;
47-
};
48-
existingScopes: Set<string>;
49-
exposedModules: {
50-
[moduleId: string]: ExposedScalprumModule;
51-
};
52-
scalprumOptions: ScalprumOptions;
53-
api: T;
54-
pluginStore: PluginStore;
55-
};
56-
57-
export type Container = Window & Factory;
58-
59-
declare function __webpack_init_sharing__(scope: string): void;
60-
declare let __webpack_share_scopes__: any;
61-
62-
const SHARED_SCOPE_NAME = 'default';
63-
64-
let scalprum: Scalprum | undefined;
65-
66-
export const getModuleIdentifier = (scope: string, module: string) => `${scope}#${module}`;
67-
68-
export const getScalprum = () => {
69-
if (!scalprum) {
70-
throw new Error('Scalprum was not initialized! Call the initialize function first.');
71-
}
72-
73-
return scalprum;
74-
};
75-
76-
export const initSharedScope = async () => __webpack_init_sharing__(SHARED_SCOPE_NAME);
77-
78-
/**
79-
* Get the webpack share scope object.
80-
*/
81-
export const getSharedScope = (enableScopeWarning?: boolean) => {
82-
if (!Object.keys(__webpack_share_scopes__).includes(SHARED_SCOPE_NAME)) {
83-
throw new Error('Attempt to access share scope object before its initialization');
84-
}
85-
86-
const sharedScope = __webpack_share_scopes__[SHARED_SCOPE_NAME];
87-
if (enableScopeWarning) {
88-
warnDuplicatePkg(sharedScope);
89-
}
90-
91-
return sharedScope;
92-
};
93-
94-
export const handlePrefetchPromise = (id: string, prefetch?: Promise<any>) => {
95-
if (prefetch) {
96-
setPendingPrefetch(id, prefetch);
97-
prefetch.finally(() => {
98-
removePrefetch(id);
99-
});
100-
}
101-
};
102-
103-
export const getCachedModule = <T = any, P = any>(scope: string, module: string): ScalprumModule<T, P> => {
104-
const moduleId = getModuleIdentifier(scope, module);
105-
try {
106-
const cachedModule = getScalprum().exposedModules[moduleId];
107-
if (!module) {
108-
return {};
109-
}
110-
111-
const prefetchID = `${scope}#${module}`;
112-
const prefetchPromise = getPendingPrefetch(prefetchID);
113-
if (prefetchPromise) {
114-
return { cachedModule, prefetchPromise };
115-
}
116-
if (cachedModule?.prefetch) {
117-
handlePrefetchPromise(prefetchID, cachedModule.prefetch(getScalprum().api));
118-
return { cachedModule, prefetchPromise: getPendingPrefetch(prefetchID) };
119-
}
120-
return { cachedModule };
121-
} catch (error) {
122-
// If something goes wrong during the cache retrieval, reload module.
123-
console.warn(`Unable to retrieve cached module ${scope} ${module}. New module will be loaded.`, error);
124-
return {};
125-
}
126-
};
127-
128-
export const setPendingPrefetch = (id: string, prefetch: Promise<any>): void => {
129-
getScalprum().pendingPrefetch[id] = prefetch;
130-
};
131-
132-
export const getPendingPrefetch = (id: string): Promise<any> | undefined => {
133-
return getScalprum().pendingPrefetch?.[id];
134-
};
135-
136-
export const removePrefetch = (id: string) => {
137-
delete getScalprum().pendingPrefetch[id];
138-
};
139-
140-
export const resolvePendingInjection = (id: string) => {
141-
delete getScalprum().pendingInjections[id];
142-
};
143-
144-
export const setPendingLoading = (scope: string, module: string, promise: Promise<any>): Promise<any> => {
145-
getScalprum().pendingLoading[`${scope}#${module}`] = promise;
146-
promise
147-
.then((data) => {
148-
delete getScalprum().pendingLoading[`${scope}#${module}`];
149-
return data;
150-
})
151-
.catch(() => {
152-
delete getScalprum().pendingLoading[`${scope}#${module}`];
153-
});
154-
return promise;
155-
};
156-
157-
export const getPendingLoading = (scope: string, module: string): Promise<any> | undefined => {
158-
return getScalprum().pendingLoading[`${scope}#${module}`];
159-
};
160-
161-
export const preloadModule = async (scope: string, module: string, processor?: (manifest: any) => string[]) => {
162-
const { manifestLocation } = getAppData(scope);
163-
const { cachedModule } = getCachedModule(scope, module);
164-
let modulePromise = getPendingLoading(scope, module);
165-
166-
// lock preloading if module exists or is already being loaded
167-
if (!modulePromise && Object.keys(cachedModule || {}).length == 0 && manifestLocation) {
168-
modulePromise = processManifest(manifestLocation, scope, module, processor).then(() => getScalprum().pluginStore.getExposedModule(scope, module));
169-
}
170-
171-
// add scalprum API later
172-
const prefetchID = `${scope}#${module}`;
173-
174-
if (!getPendingPrefetch(prefetchID) && cachedModule?.prefetch) {
175-
handlePrefetchPromise(prefetchID, cachedModule.prefetch(getScalprum().api));
176-
}
177-
178-
return setPendingLoading(scope, module, Promise.resolve(modulePromise));
179-
};
180-
181-
export const getModule = async <T = any, P = any>(scope: string, module: string, importName = 'default'): Promise<T> => {
182-
const scalprum = getScalprum();
183-
const { cachedModule } = getCachedModule(scope, module);
184-
let Module: ExposedScalprumModule<T, P>;
185-
const manifestLocation = getAppData(scope)?.manifestLocation;
186-
if (!manifestLocation) {
187-
throw new Error(`Could not get module. Manifest location not found for scope ${scope}.`);
188-
}
189-
if (!cachedModule) {
190-
try {
191-
await processManifest(manifestLocation, scope, module);
192-
Module = await scalprum.pluginStore.getExposedModule(scope, module);
193-
} catch {
194-
throw new Error(
195-
`Module not initialized! Module "${module}" was not found in "${scope}" webpack scope. Make sure the remote container is loaded?`,
196-
);
197-
}
198-
} else {
199-
Module = cachedModule;
200-
}
201-
202-
return Module[importName];
203-
};
204-
205-
export const initialize = <T extends Record<string, any> = Record<string, any>>({
206-
appsConfig,
207-
api,
208-
options,
209-
pluginStoreFeatureFlags = {},
210-
pluginLoaderOptions = {},
211-
pluginStoreOptions = {},
212-
}: {
213-
appsConfig: AppsConfig;
214-
api?: T;
215-
options?: Partial<ScalprumOptions>;
216-
pluginStoreFeatureFlags?: FeatureFlags;
217-
pluginLoaderOptions?: PluginLoaderOptions;
218-
pluginStoreOptions?: PluginStoreOptions;
219-
}): Scalprum<T> => {
220-
if (scalprum) {
221-
scalprum.api = api || {};
222-
scalprum.appsConfig = appsConfig;
223-
scalprum.scalprumOptions = {
224-
...scalprum.scalprumOptions,
225-
...options,
226-
};
227-
scalprum.pluginStore.setFeatureFlags(pluginStoreFeatureFlags);
228-
return scalprum as Scalprum<T>;
229-
}
230-
const defaultOptions: ScalprumOptions = {
231-
cacheTimeout: 120,
232-
enableScopeWarning: global?.process?.env?.NODE_ENV === 'development',
233-
...options,
234-
};
235-
236-
// Create new plugin store
237-
const pluginStore = new PluginStore({
238-
...pluginStoreOptions,
239-
loaderOptions: {
240-
sharedScope: getSharedScope(defaultOptions.enableScopeWarning),
241-
getPluginEntryModule: ({ name }) => (window as { [key: string]: any })[name],
242-
...pluginLoaderOptions,
243-
},
244-
});
245-
pluginStore.setFeatureFlags(pluginStoreFeatureFlags);
246-
247-
scalprum = {
248-
appsConfig,
249-
pendingInjections: {},
250-
pendingLoading: {},
251-
pendingPrefetch: {},
252-
existingScopes: new Set<string>(),
253-
exposedModules: {},
254-
scalprumOptions: defaultOptions,
255-
api: api || {},
256-
pluginStore,
257-
};
258-
259-
return scalprum as Scalprum<T>;
260-
};
261-
262-
export const removeScalprum = () => {
263-
scalprum = undefined;
264-
};
265-
266-
export const getAppData = (name: string): AppMetadata => getScalprum().appsConfig[name];
267-
268-
const setExposedModule = (scope: string, module: string, exposedModule: ExposedScalprumModule) => {
269-
if (!getScalprum().existingScopes.has(scope)) {
270-
getScalprum().existingScopes.add(scope);
271-
}
272-
const moduleId = getModuleIdentifier(scope, module);
273-
getScalprum().exposedModules[moduleId] = exposedModule;
274-
};
275-
276-
const clearPendingInjection = (scope: string) => {
277-
delete getScalprum().pendingInjections[scope];
278-
};
279-
280-
const setPendingInjection = (scope: string, promise: Promise<any>) => {
281-
getScalprum().pendingInjections[scope] = promise;
282-
};
283-
284-
const getPendingInjection = (scope: string): Promise<any> | undefined => getScalprum().pendingInjections[scope];
285-
286-
// PluginManifest typeguard
287-
function isPluginManifest(manifest: any): manifest is PluginManifest {
288-
return (
289-
typeof manifest.name === 'string' &&
290-
typeof manifest.version === 'string' &&
291-
Array.isArray(manifest.extensions) &&
292-
Array.isArray(manifest.loadScripts)
293-
);
294-
}
295-
296-
function extractBaseURL(path: string) {
297-
const result = path.split('/');
298-
// remove last section of pathname that includes the JS filename
299-
result.pop();
300-
// make sure there is always at least leading / to satisfy sdk manifest validation
301-
return result.join('/') || '/';
302-
}
303-
304-
export async function processManifest(
305-
moduleManifest: string | PluginManifest,
306-
scope: string,
307-
module: string,
308-
processor?: (manifest: any) => string[],
309-
): Promise<void> {
310-
let pendingInjection = getPendingInjection(scope);
311-
const { pluginStore, existingScopes } = getScalprum();
312-
313-
if (existingScopes.has(scope)) {
314-
try {
315-
const exposedModule = await pluginStore.getExposedModule<ExposedScalprumModule>(scope, module);
316-
setExposedModule(scope, module, exposedModule);
317-
return;
318-
} catch (error) {
319-
console.warn('Unable to load module from existing container', error);
320-
console.warn('Scalprum will try to process manifest from scratch.');
321-
}
322-
}
323-
324-
if (pendingInjection) {
325-
await pendingInjection;
326-
const exposedModule = await pluginStore.getExposedModule<ExposedScalprumModule>(scope, module);
327-
setExposedModule(scope, module, exposedModule);
328-
return;
329-
}
330-
331-
pendingInjection = (async () => {
332-
let manifest: PluginManifest | { [scope: string]: { entry: string[] } };
333-
if (typeof moduleManifest === 'object') {
334-
manifest = moduleManifest;
335-
} else {
336-
const headers = new Headers();
337-
headers.append('Pragma', 'no-cache');
338-
headers.append('Cache-Control', 'no-cache');
339-
headers.append('expires', '0');
340-
const manifestPromise = await fetch(moduleManifest, {
341-
method: 'GET',
342-
headers,
343-
});
344-
// handle network errors
345-
if (!manifestPromise.ok) {
346-
const resClone = manifestPromise.clone();
347-
let data;
348-
try {
349-
data = await resClone.json();
350-
} catch (error) {
351-
throw new Error(`Unable to load manifest files at ${moduleManifest}! ${resClone.status}: ${resClone.statusText}`);
352-
}
353-
throw new Error(`Unable to load manifest files at ${moduleManifest}! ${data}`);
354-
}
355-
try {
356-
manifest = await manifestPromise.json();
357-
} catch (error) {
358-
clearPendingInjection(scope);
359-
throw new Error(error as string);
360-
}
361-
}
362-
let sdkManifest: PluginManifest;
363-
if (isPluginManifest(manifest)) {
364-
sdkManifest = manifest;
365-
} else {
366-
const loadScripts: string[] = processor ? processor(manifest) : manifest[scope].entry;
367-
const baseURL = extractBaseURL(loadScripts[0]);
368-
sdkManifest = {
369-
extensions: [],
370-
// remove base URL from script entry, baseURL is added by scalprum provider
371-
loadScripts: loadScripts.map((script) => script.replace(baseURL, '')),
372-
name: scope,
373-
registrationMethod: 'custom',
374-
version: '1.0.0',
375-
baseURL,
376-
};
377-
}
378-
379-
await pluginStore.loadPlugin(sdkManifest);
380-
try {
381-
const exposedModule = await pluginStore.getExposedModule<ExposedScalprumModule>(scope, module);
382-
setExposedModule(scope, module, exposedModule);
383-
return;
384-
} catch (error) {
385-
clearPendingInjection(scope);
386-
throw error;
387-
}
388-
})();
389-
390-
setPendingInjection(scope, pendingInjection);
391-
await pendingInjection;
392-
clearPendingInjection(scope);
393-
}
1+
export * from './scalprum';
2+
export * from './createSharedStore';
3+
export * from './warnDuplicatePkg';

0 commit comments

Comments
 (0)