@equinor/fusion-framework 8.1.0-next.2 → 8.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,273 +0,0 @@
1
- import { type AnyModule, ModulesConfigurator } from '@equinor/fusion-framework-module';
2
-
3
- import event from '@equinor/fusion-framework-module-event';
4
-
5
- import http, {
6
- configureHttpClient,
7
- configureHttp,
8
- type HttpClientOptions,
9
- } from '@equinor/fusion-framework-module-http';
10
- import type { HttpClientMsal } from '@equinor/fusion-framework-module-http/client';
11
-
12
- import auth, { type AuthConfigFn } from '@equinor/fusion-framework-module-msal';
13
-
14
- import context from '@equinor/fusion-framework-module-context';
15
-
16
- import disco from '@equinor/fusion-framework-module-service-discovery';
17
- import services from '@equinor/fusion-framework-module-services';
18
- import telemetry, { enableTelemetry } from '@equinor/fusion-framework-module-telemetry';
19
-
20
- import type { FusionModules } from './types.js';
21
- import type { MsalClientConfig } from '@equinor/fusion-framework-module-msal';
22
- import { version } from './version.js';
23
-
24
- /**
25
- * Configurator that registers and wires the core Fusion Framework modules.
26
- *
27
- * Extend `FrameworkConfigurator` to declare which modules (authentication,
28
- * HTTP, service-discovery, telemetry, context, …) the framework instance
29
- * should contain and how each module is configured before the framework
30
- * starts.
31
- *
32
- * Typical workflow:
33
- * 1. Create a `FrameworkConfigurator` instance.
34
- * 2. Call configuration helpers such as {@link configureMsal},
35
- * {@link configureHttp}, {@link configureHttpClient}, and
36
- * {@link configureServiceDiscovery}.
37
- * 3. Pass the configurator to {@link init} to bootstrap the framework.
38
- *
39
- * @template TModules - Additional module descriptors to merge with the
40
- * built-in Fusion modules.
41
- * @template TRef - Optional reference object forwarded to module
42
- * initialization (e.g. a parent framework instance).
43
- *
44
- * @example
45
- * ```typescript
46
- * import { FrameworkConfigurator, init } from '@equinor/fusion-framework';
47
- *
48
- * const configurator = new FrameworkConfigurator();
49
- * configurator.configureMsal({ clientId: 'my-client-id', authority: '…' });
50
- * configurator.configureServiceDiscovery({
51
- * client: { baseUri: 'https://service-registry.example.com' },
52
- * });
53
- *
54
- * const fusion = await init(configurator);
55
- * ```
56
- */
57
- export class FrameworkConfigurator<
58
- TModules extends Array<AnyModule> = [],
59
- // biome-ignore lint/suspicious/noExplicitAny: default must be bivariant `any`, not `unknown` \u2014 `unknown` breaks assignability when a concrete `FrameworkConfigurator<TModules, TRef>` is used where the default-typed class is expected
60
- TRef = any,
61
- > extends ModulesConfigurator<FusionModules<TModules>, TRef> {
62
- /**
63
- * The class name used for event naming. This static property ensures
64
- * the name is preserved through compilation and minification.
65
- */
66
- static readonly className: string = 'FrameworkConfigurator';
67
-
68
- /**
69
- * Creates a new FrameworkConfigurator instance with default telemetry configuration.
70
- *
71
- * Initializes the framework with core modules (event, auth, http, service discovery,
72
- * services, context, and telemetry) and sets up default telemetry that includes
73
- * framework version metadata and 'framework' scope for all telemetry events.
74
- *
75
- * @example
76
- * ```typescript
77
- * const configurator = new FrameworkConfigurator();
78
- * // Now ready to configure additional modules
79
- * ```
80
- */
81
- constructor() {
82
- super([event, auth, http, disco, services, context, telemetry]);
83
-
84
- // default configuration
85
- enableTelemetry(this, {
86
- configure: (builder) => {
87
- builder.setMetadata({
88
- fusion: {
89
- type: 'framework-telemetry',
90
- framework: {
91
- version,
92
- },
93
- },
94
- });
95
- builder.setDefaultScope(['framework']);
96
- },
97
- });
98
- }
99
-
100
- /**
101
- * Configures the global HTTP module settings such as base URLs, default headers,
102
- * request/response interceptors, and timeout settings.
103
- *
104
- * This affects all HTTP requests made through the framework's HTTP module.
105
- * Use this for application-wide HTTP configuration.
106
- *
107
- * @param args - HTTP module configuration arguments (same as configureHttp function)
108
- *
109
- * @example
110
- * ```typescript
111
- * configurator.configureHttp({
112
- * baseUri: 'https://api.example.com',
113
- * defaultHeaders: { 'X-App-Version': '1.0.0' }
114
- * });
115
- * ```
116
- */
117
- public configureHttp(...args: Parameters<typeof configureHttp>) {
118
- this.addConfig(configureHttp(...args));
119
- }
120
-
121
- /**
122
- * Configures a named HTTP client instance with specific settings.
123
- *
124
- * Unlike configureHttp which sets global HTTP settings, this creates a named client
125
- * that can have its own base URL, headers, interceptors, and other HTTP-specific
126
- * configuration. Useful for connecting to different APIs or services with different requirements.
127
- *
128
- * @param args - HTTP client configuration arguments: [name, clientOptions]
129
- * where name is a string identifier and clientOptions contains HTTP settings
130
- *
131
- * @example
132
- * ```typescript
133
- * // Configure a client for external API
134
- * configurator.configureHttpClient('external-api', {
135
- * baseUri: 'https://external-api.com',
136
- * defaultHeaders: { 'Authorization': 'Bearer token' }
137
- * });
138
- *
139
- * // Configure a client for internal services
140
- * configurator.configureHttpClient('internal', {
141
- * baseUri: 'https://internal.company.com',
142
- * timeout: 10000
143
- * });
144
- * ```
145
- */
146
- public configureHttpClient(...args: Parameters<typeof configureHttpClient>) {
147
- this.addConfig(configureHttpClient(...args));
148
- }
149
-
150
- /**
151
- * Configures Microsoft Authentication Library (MSAL) authentication for the framework.
152
- *
153
- * This sets up OAuth 2.0 / OpenID Connect authentication using Azure AD or other
154
- * MSAL-compatible identity providers. The authentication module will handle token
155
- * acquisition, refresh, and user session management.
156
- *
157
- * @param cb_or_config - Authentication configuration. Can be either:
158
- * - A callback function that receives an auth builder for advanced configuration
159
- * - A client configuration object with MSAL settings
160
- * @param requiresAuth - Whether the application requires authentication to function.
161
- * When true (default), unauthenticated users cannot access the app.
162
- * When false, authentication is optional but available when needed.
163
- *
164
- * @example
165
- * ```typescript
166
- * // Simple configuration with client config object
167
- * configurator.configureMsal({
168
- * clientId: 'your-client-id',
169
- * authority: 'https://login.microsoftonline.com/your-tenant-id'
170
- * });
171
- *
172
- * // Advanced configuration with callback
173
- * configurator.configureMsal((builder) => {
174
- * builder.setClientConfig({
175
- * clientId: 'your-client-id',
176
- * authority: 'https://login.microsoftonline.com/your-tenant-id'
177
- * });
178
- * builder.setScopes(['User.Read', 'api://your-api/scope']);
179
- * builder.setRequiresAuth(true);
180
- * });
181
- *
182
- * // Optional authentication
183
- * configurator.configureMsal(config, false);
184
- * ```
185
- */
186
- public configureMsal(cb_or_config: AuthConfigFn | MsalClientConfig, requiresAuth = true) {
187
- this.addConfig({
188
- module: auth,
189
- configure: (builder) => {
190
- // Only override the default requiresAuth flag when the caller explicitly set one
191
- if (requiresAuth !== undefined) {
192
- builder.setRequiresAuth(!!requiresAuth);
193
- }
194
- // A function argument configures the builder directly
195
- if (typeof cb_or_config === 'function') {
196
- cb_or_config(builder);
197
- }
198
- // A plain object argument is applied as the MSAL client config
199
- if (typeof cb_or_config === 'object') {
200
- builder.setClientConfig(cb_or_config);
201
- }
202
- },
203
- });
204
- }
205
-
206
- /**
207
- * Configures the service discovery module to automatically find and connect to services.
208
- *
209
- * Service discovery allows the framework to dynamically locate microservices and APIs
210
- * at runtime rather than hardcoding their URLs. This is essential in distributed systems
211
- * where services may be deployed across multiple environments or scaled dynamically.
212
- *
213
- * @param args - Configuration object for service discovery
214
- * @param args.client - HTTP client configuration used to communicate with the
215
- * service discovery registry. Typically includes base URL,
216
- * authentication, and timeout settings.
217
- *
218
- * @example
219
- * ```typescript
220
- * configurator.configureServiceDiscovery({
221
- * client: {
222
- * baseUri: 'https://service-registry.company.com',
223
- * defaultHeaders: { 'Authorization': 'Bearer token' },
224
- * timeout: 5000
225
- * }
226
- * });
227
- * ```
228
- */
229
- public configureServiceDiscovery(args: { client: HttpClientOptions<HttpClientMsal> }) {
230
- this.configureHttpClient('service_discovery', args.client);
231
- }
232
-
233
- /**
234
- * Configures application telemetry and observability settings.
235
- *
236
- * Telemetry enables tracking of application usage, performance metrics, errors,
237
- * and custom events. This helps with monitoring, debugging, and understanding
238
- * how your application is being used. The framework comes with default telemetry
239
- * that includes framework version and scope information.
240
- *
241
- * @param cb - Configuration callback function that receives a telemetry builder.
242
- * Use this to customize telemetry settings like event scopes, metadata,
243
- * sampling rates, and custom instrumentation.
244
- *
245
- * @example
246
- * ```typescript
247
- * configurator.configureTelemetry((builder) => {
248
- * // Add custom metadata to all telemetry events
249
- * builder.setMetadata({
250
- * application: 'my-app',
251
- * environment: 'production',
252
- * version: '1.2.3'
253
- * });
254
- *
255
- * // Set custom scopes for filtering events
256
- * builder.setDefaultScope(['app', 'performance']);
257
- *
258
- * // Configure sampling (only send 10% of events)
259
- * builder.setSamplingRate(0.1);
260
- *
261
- * // Add custom instrumentation
262
- * builder.addInstrumentation('custom-operation', (context) => {
263
- * // Track custom metrics
264
- * });
265
- * });
266
- * ```
267
- */
268
- public configureTelemetry(cb: Parameters<typeof enableTelemetry>[1]): void {
269
- enableTelemetry(this, cb);
270
- }
271
- }
272
-
273
- export default FrameworkConfigurator;
@@ -1,35 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { firstValueFrom, take } from 'rxjs';
3
- import { FrameworkConfigurator } from '../FrameworkConfigurator.js';
4
- import type { AnyModule } from '@equinor/fusion-framework-module';
5
- import { SemanticVersion } from '@equinor/fusion-framework-module';
6
-
7
- describe('FrameworkConfigurator', () => {
8
- let configurator: FrameworkConfigurator;
9
-
10
- // Create a mock module for testing
11
- const createMockModule = (name: string, version = '1.0.0'): AnyModule => ({
12
- name,
13
- version: new SemanticVersion(version),
14
- initialize: () => ({ mockInstance: true }),
15
- });
16
-
17
- beforeEach(() => {
18
- configurator = new FrameworkConfigurator();
19
- });
20
-
21
- describe('Event Name Prefixing', () => {
22
- it('should prefix event names with "FrameworkConfigurator::"', async () => {
23
- // Trigger event by adding a config
24
- configurator.addConfig({
25
- module: createMockModule('test', '1.0.0'),
26
- configure: () => {},
27
- });
28
-
29
- // Wait for the first event to be emitted
30
- const event = await firstValueFrom(configurator.event$.pipe(take(1)));
31
-
32
- expect(event.name).toMatch(/^FrameworkConfigurator::/);
33
- });
34
- });
35
- });
@@ -1,87 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import type { IModulesConfigurator, Module } from '@equinor/fusion-framework-module';
4
-
5
- import { mockFramework } from '../../mock/index.js';
6
-
7
- /**
8
- * A module an application team owns, which the framework knows nothing about.
9
- *
10
- * @remarks
11
- * The point of these tests is that nothing below required support from
12
- * `@equinor/fusion-framework/mock`. If an application team can do this, the
13
- * extension pattern holds.
14
- */
15
- interface InvoiceClient {
16
- getInvoice(id: string): Promise<{ id: string; total: number }>;
17
- }
18
-
19
- class InvoiceConfigurator {
20
- #client?: InvoiceClient;
21
-
22
- public setClient(client: InvoiceClient): void {
23
- this.#client = client;
24
- }
25
-
26
- public createClient(): InvoiceClient {
27
- if (!this.#client) {
28
- throw new Error('An invoice client is required');
29
- }
30
- return this.#client;
31
- }
32
- }
33
-
34
- type InvoiceModule = Module<'invoices', InvoiceClient, InvoiceConfigurator>;
35
-
36
- const invoiceModule: InvoiceModule = {
37
- name: 'invoices',
38
- configure: () => new InvoiceConfigurator(),
39
- initialize: ({ config }) => config.createClient(),
40
- };
41
-
42
- /** The team's own mock, following the pattern the built-in modules use. */
43
- const enableInvoicesMock = (
44
- // biome-ignore lint/suspicious/noExplicitAny: mirrors every enableX helper
45
- configurator: IModulesConfigurator<any, any>,
46
- options: { total?: number } = {},
47
- ): void => {
48
- configurator.addConfig({
49
- module: invoiceModule,
50
- configure: (builder: InvoiceConfigurator) =>
51
- builder.setClient({
52
- getInvoice: async (id) => ({ id, total: options.total ?? 0 }),
53
- }),
54
- } as { module: InvoiceModule });
55
- };
56
-
57
- describe('application modules', () => {
58
- it('fails to start without its mock, so the seam is real and not a no-op', async () => {
59
- await expect(
60
- mockFramework<[InvoiceModule]>((configurator) => {
61
- configurator.addConfig({ module: invoiceModule } as { module: InvoiceModule });
62
- }),
63
- ).rejects.toThrow(/invoice client is required/i);
64
- });
65
-
66
- it('composes with the built-in mocks and is typed without a cast', async () => {
67
- const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
68
- configurator.msal.setAccount({ name: 'Ada Lovelace' });
69
- enableInvoicesMock(configurator, { total: 42 });
70
- });
71
-
72
- // No cast: `TModules` must flow through to `fusion.modules`.
73
- const invoice = await fusion.modules.invoices.getInvoice('inv-1');
74
-
75
- expect(invoice).toEqual({ id: 'inv-1', total: 42 });
76
- expect(fusion.modules.auth.account?.name).toBe('Ada Lovelace');
77
- });
78
-
79
- it('is registered exactly as it is in production', async () => {
80
- // The same helper, against a real FrameworkConfigurator, would behave identically.
81
- const fusion = await mockFramework<[InvoiceModule]>((configurator) =>
82
- enableInvoicesMock(configurator, { total: 7 }),
83
- );
84
-
85
- await expect(fusion.modules.invoices.getInvoice('inv-2')).resolves.toMatchObject({ total: 7 });
86
- });
87
- });
@@ -1,98 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- import { decodeJwtSegment } from '@equinor/fusion-framework-module-msal/mock';
4
-
5
- import { init } from '../../init.js';
6
- import { createMockService, FrameworkMockConfigurator, mockFramework } from '../../mock/index.js';
7
-
8
- /**
9
- * Pins the snippets in `docs/testing.md` and the package README to real
10
- * behaviour, so documentation cannot drift away from the API it describes.
11
- */
12
- describe('documented usage', () => {
13
- afterEach(() => vi.restoreAllMocks());
14
-
15
- it('composes the service registry on the builder', async () => {
16
- const fusion = await mockFramework((configurator) => {
17
- configurator.serviceDiscovery.setBaseUri('http://localhost:6669');
18
- configurator.serviceDiscovery.addService({ key: 'my-api' });
19
- configurator.serviceDiscovery.removeService('bookmarks');
20
- });
21
-
22
- await expect(fusion.modules.serviceDiscovery.resolveService('my-api')).resolves.toMatchObject({
23
- key: 'my-api',
24
- });
25
- });
26
-
27
- it('replaces the baseline registry with setServices', async () => {
28
- const fusion = await mockFramework((configurator) => {
29
- configurator.serviceDiscovery.setServices([{ key: 'apps', uri: 'http://localhost:3000' }]);
30
- });
31
-
32
- await expect(fusion.modules.serviceDiscovery.resolveService('apps')).resolves.toMatchObject({
33
- uri: 'http://localhost:3000',
34
- });
35
- });
36
-
37
- it('keeps startup working when the registry is replaced, because unknown services synthesise', async () => {
38
- // The context module resolves `context` while initializing, so a replaced
39
- // registry that omits it would break start-up if synthesis were disabled.
40
- const fusion = await mockFramework((configurator) => {
41
- configurator.serviceDiscovery.setServices([{ key: 'apps', uri: 'http://localhost:3000' }]);
42
- });
43
-
44
- await expect(fusion.modules.serviceDiscovery.resolveService('people')).resolves.toBeDefined();
45
- });
46
-
47
- it('lets the test runner spy on the discovery client', async () => {
48
- const fusion = await mockFramework();
49
-
50
- vi.spyOn(fusion.modules.serviceDiscovery.client, 'resolveService').mockResolvedValue(
51
- createMockService({ key: 'apps', uri: 'http://spied' }),
52
- );
53
-
54
- await expect(fusion.modules.serviceDiscovery.resolveService('apps')).resolves.toMatchObject({
55
- uri: 'http://spied',
56
- });
57
- });
58
-
59
- it('lets the test runner spy on the auth client', async () => {
60
- const fusion = await mockFramework();
61
-
62
- const spy = vi.spyOn(fusion.modules.auth.client, 'acquireToken');
63
-
64
- await fusion.modules.auth.acquireAccessToken({ request: { scopes: ['Files.Read'] } });
65
-
66
- expect(spy).toHaveBeenCalled();
67
- });
68
-
69
- it('resolves the real scope through the real provider, not the test double', async () => {
70
- const fusion = await mockFramework((configurator) => {
71
- // The client is configured with what it talks to, exactly as in production
72
- configurator.msal.setClientConfig({ auth: { clientId: 'my-app', tenantId: 'my-tenant' } });
73
- });
74
-
75
- const token = await fusion.modules.auth.acquireAccessToken();
76
- const claims = JSON.parse(decodeJwtSegment(token?.split('.')[1] ?? ''));
77
-
78
- // MsalProvider — not the mock — turns "no scopes requested" into `${clientId}/.default`
79
- expect(claims.scp).toBe('my-app/.default');
80
- });
81
-
82
- it('signs nobody in when the account is signed out', async () => {
83
- const fusion = await mockFramework((configurator) => {
84
- configurator.msal.setAccount({ signedOut: true });
85
- });
86
-
87
- expect(fusion.modules.auth.account).toBeFalsy();
88
- });
89
-
90
- it('can be constructed directly and initialized with init', async () => {
91
- const configurator = new FrameworkMockConfigurator();
92
- configurator.msal.setAccount({ name: 'Ada Lovelace' });
93
-
94
- const fusion = await init(configurator);
95
-
96
- expect(fusion.modules.auth.account?.name).toBe('Ada Lovelace');
97
- });
98
- });