@equinor/fusion-framework-vite-plugin-spa 4.1.0 → 4.1.2
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.
- package/dist/esm/version.js +1 -1
- package/dist/html/bootstrap.js +16 -18
- package/dist/html/bootstrap.js.map +1 -1
- package/dist/{index-B_3FYBBq.js → index-DYAtJv_q.js} +2 -2
- package/dist/{index-B_3FYBBq.js.map → index-DYAtJv_q.js.map} +1 -1
- package/dist/{module-w44_1T9j.js → module-DK5kAoXA.js} +3661 -11812
- package/dist/module-DK5kAoXA.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/html/html.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +9 -6
- package/CHANGELOG.md +0 -874
- package/dist/module-w44_1T9j.js.map +0 -1
- package/rollup.config.js +0 -20
- package/src/html/bootstrap.ts +0 -209
- package/src/html/create-portal-entry-point.ts +0 -25
- package/src/html/html.ts +0 -55
- package/src/html/index.ts +0 -10
- package/src/html/is-enabled-env-value.ts +0 -9
- package/src/html/register-service-worker.ts +0 -228
- package/src/html/sw.ts +0 -238
- package/src/index.ts +0 -34
- package/src/plugin.ts +0 -202
- package/src/types.ts +0 -160
- package/src/util/load-environment.ts +0 -35
- package/src/util/object-to-env.ts +0 -44
- package/src/version.ts +0 -2
- package/tests/create-portal-entry-point.test.ts +0 -36
- package/tests/is-enabled-env-value.test.ts +0 -13
- package/tsconfig.json +0 -35
- package/vitest.config.ts +0 -10
package/rollup.config.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
-
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
-
|
|
4
|
-
/** @type {import('rollup').RollupOptions} */
|
|
5
|
-
export default [
|
|
6
|
-
{
|
|
7
|
-
input: {
|
|
8
|
-
'html/bootstrap': 'dist/esm/html/bootstrap.js',
|
|
9
|
-
'html/sw': 'dist/esm/html/sw.js',
|
|
10
|
-
},
|
|
11
|
-
output: {
|
|
12
|
-
format: 'esm',
|
|
13
|
-
dir: 'dist',
|
|
14
|
-
entryFileNames: '[name].js',
|
|
15
|
-
sourcemap: true,
|
|
16
|
-
strict: false,
|
|
17
|
-
},
|
|
18
|
-
plugins: [resolve({ preferBuiltins: true }), commonjs()],
|
|
19
|
-
},
|
|
20
|
-
];
|
package/src/html/bootstrap.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { ModulesConfigurator } from '@equinor/fusion-framework-module';
|
|
2
|
-
|
|
3
|
-
import { configureHttpClient, type HttpModule } from '@equinor/fusion-framework-module-http';
|
|
4
|
-
import { enableMSAL, type MsalModule } from '@equinor/fusion-framework-module-msal';
|
|
5
|
-
import {
|
|
6
|
-
enableServiceDiscovery,
|
|
7
|
-
type ServiceDiscoveryModule,
|
|
8
|
-
} from '@equinor/fusion-framework-module-service-discovery';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
enableTelemetry,
|
|
12
|
-
TelemetryLevel,
|
|
13
|
-
type TelemetryModule,
|
|
14
|
-
} from '@equinor/fusion-framework-module-telemetry';
|
|
15
|
-
import { ConsoleAdapter } from '@equinor/fusion-framework-module-telemetry/console-adapter';
|
|
16
|
-
|
|
17
|
-
import { createPortalEntryPoint } from './create-portal-entry-point.js';
|
|
18
|
-
import { isEnabledEnvValue } from './is-enabled-env-value.js';
|
|
19
|
-
import { registerServiceWorker } from './register-service-worker.js';
|
|
20
|
-
|
|
21
|
-
import { version } from '../version.js';
|
|
22
|
-
|
|
23
|
-
// TODO(#5065): add type for portal manifest when available
|
|
24
|
-
type PortalManifest = {
|
|
25
|
-
build: {
|
|
26
|
-
config: Record<string, unknown>;
|
|
27
|
-
templateEntry: string;
|
|
28
|
-
assetPath?: string;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Allow dynamic import without vite
|
|
33
|
-
const importWithoutVite = <T>(path: string): Promise<T> => import(/* @vite-ignore */ path);
|
|
34
|
-
|
|
35
|
-
// Create Fusion Framework configurator
|
|
36
|
-
const configurator = new ModulesConfigurator();
|
|
37
|
-
|
|
38
|
-
const serviceDiscoveryUrl = new URL(
|
|
39
|
-
import.meta.env.FUSION_SPA_SERVICE_DISCOVERY_URL,
|
|
40
|
-
import.meta.env.FUSION_SPA_SERVICE_DISCOVERY_URL.startsWith('http')
|
|
41
|
-
? undefined
|
|
42
|
-
: window.location.origin,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// define service discovery client - this is used in the service discovery module
|
|
46
|
-
configurator.addConfig(
|
|
47
|
-
configureHttpClient('service_discovery', {
|
|
48
|
-
baseUri: String(serviceDiscoveryUrl),
|
|
49
|
-
defaultScopes: import.meta.env.FUSION_SPA_SERVICE_DISCOVERY_SCOPES,
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// setup service discovery - enable service discovery for the framework
|
|
54
|
-
enableServiceDiscovery(configurator, async (builder) => {
|
|
55
|
-
builder.configureServiceDiscoveryClientByClientKey('service_discovery');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Avoid interactive Entra ID authentication when the SPA runs in CI or Playwright.
|
|
59
|
-
if (isEnabledEnvValue(import.meta.env.FUSION_SPA_MSAL_MOCK)) {
|
|
60
|
-
const { enableMsalMock } = await import('@equinor/fusion-framework-module-msal/mock');
|
|
61
|
-
|
|
62
|
-
const mockToken = import.meta.env.FUSION_SPA_MSAL_MOCK_TOKEN;
|
|
63
|
-
|
|
64
|
-
enableMsalMock(configurator, (builder) => {
|
|
65
|
-
// Preserve the mock client's default identity when no backend-specific token is configured.
|
|
66
|
-
if (!mockToken) return;
|
|
67
|
-
|
|
68
|
-
builder.setToken(mockToken);
|
|
69
|
-
});
|
|
70
|
-
} else {
|
|
71
|
-
enableMSAL(configurator, (builder) => {
|
|
72
|
-
builder.setClientConfig({
|
|
73
|
-
auth: {
|
|
74
|
-
clientId: import.meta.env.FUSION_SPA_MSAL_CLIENT_ID,
|
|
75
|
-
tenantId: import.meta.env.FUSION_SPA_MSAL_TENANT_ID,
|
|
76
|
-
redirectUri: import.meta.env.FUSION_SPA_MSAL_REDIRECT_URI,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
builder.setRequiresAuth(isEnabledEnvValue(import.meta.env.FUSION_SPA_MSAL_REQUIRES_AUTH));
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
enableTelemetry(configurator, {
|
|
85
|
-
attachConfiguratorEvents: true,
|
|
86
|
-
configure: (builder) => {
|
|
87
|
-
const consoleLevel = Number(
|
|
88
|
-
import.meta.env.FUSION_SPA_TELEMETRY_CONSOLE_LEVEL ?? TelemetryLevel.Information,
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const consoleAdapter = new ConsoleAdapter({
|
|
92
|
-
filter: Number.isNaN(consoleLevel) ? undefined : (item) => item.level >= consoleLevel,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
builder.setAdapter('console', consoleAdapter);
|
|
96
|
-
|
|
97
|
-
builder.setMetadata(({ modules }) => {
|
|
98
|
-
const metadata = {
|
|
99
|
-
fusion: {
|
|
100
|
-
spa: {
|
|
101
|
-
version,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
// biome-ignore lint/suspicious/noExplicitAny: we need to use any here to allow dynamic properties
|
|
105
|
-
} as Record<string, any>;
|
|
106
|
-
// Only attach user metadata when the auth module has resolved an account
|
|
107
|
-
if (modules?.auth) {
|
|
108
|
-
metadata.fusion.user = {
|
|
109
|
-
id: modules.auth.account?.homeAccountId,
|
|
110
|
-
name: modules.auth.account?.name,
|
|
111
|
-
email: modules.auth.account?.username,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return metadata;
|
|
115
|
-
});
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
(async () => {
|
|
120
|
-
// initialize the framework - this will create the framework instance and configure the modules
|
|
121
|
-
const ref =
|
|
122
|
-
await configurator.initialize<
|
|
123
|
-
[ServiceDiscoveryModule, HttpModule, MsalModule, TelemetryModule]
|
|
124
|
-
>();
|
|
125
|
-
|
|
126
|
-
const telemetry = ref.telemetry;
|
|
127
|
-
|
|
128
|
-
// attach service discovery to the framework - append auth token to configured endpoints
|
|
129
|
-
using measurement = telemetry.measure({
|
|
130
|
-
name: 'bootstrap',
|
|
131
|
-
level: TelemetryLevel.Information,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await measurement.clone().resolve(registerServiceWorker(ref), {
|
|
135
|
-
data: {
|
|
136
|
-
level: TelemetryLevel.Debug,
|
|
137
|
-
name: 'bootstrap::registerServiceWorker',
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// create a client for the portal service - this is used to fetch the portal manifest
|
|
142
|
-
const portalClient = await ref.serviceDiscovery.createClient('portal-config');
|
|
143
|
-
|
|
144
|
-
// fetch the portal manifest - this is used to load the portal template
|
|
145
|
-
const portalId = import.meta.env.FUSION_SPA_PORTAL_ID;
|
|
146
|
-
const portalTag = import.meta.env.FUSION_SPA_PORTAL_TAG ?? 'latest';
|
|
147
|
-
const portalProxy = import.meta.env.FUSION_SPA_PORTAL_PROXY ?? false;
|
|
148
|
-
const portal_manifest = await measurement
|
|
149
|
-
.clone()
|
|
150
|
-
.resolve(portalClient.json<PortalManifest>(`/portals/${portalId}@${portalTag}`), {
|
|
151
|
-
data: (manifest: PortalManifest) => ({
|
|
152
|
-
name: 'bootstrap::loadPortalManifest',
|
|
153
|
-
level: TelemetryLevel.Debug,
|
|
154
|
-
properties: {
|
|
155
|
-
portalId,
|
|
156
|
-
portalTag,
|
|
157
|
-
templateEntry: manifest.build.templateEntry,
|
|
158
|
-
manifestVersion: manifest.build.config?.version,
|
|
159
|
-
assetPath: manifest.build.assetPath,
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const portal_config = await measurement
|
|
165
|
-
.clone()
|
|
166
|
-
.resolve(portalClient.json(`/portals/${portalId}@${portalTag}/config`), {
|
|
167
|
-
data: {
|
|
168
|
-
name: 'bootstrap::loadPortalConfig',
|
|
169
|
-
level: TelemetryLevel.Debug,
|
|
170
|
-
properties: {
|
|
171
|
-
portalId,
|
|
172
|
-
portalTag,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// create a entrypoint for the portal - this is used to render the portal
|
|
178
|
-
const el = document.createElement('div');
|
|
179
|
-
document.body.innerHTML = '';
|
|
180
|
-
document.body.appendChild(el);
|
|
181
|
-
|
|
182
|
-
const portalEntryPoint = createPortalEntryPoint(
|
|
183
|
-
portalProxy ? '/portal-proxy' : undefined,
|
|
184
|
-
portal_manifest.build.assetPath,
|
|
185
|
-
portal_manifest.build.templateEntry,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// TODO(#5066): should test if the entrypoint is external or internal
|
|
189
|
-
// TODO(#5066): add proper return type
|
|
190
|
-
const { render } = await measurement
|
|
191
|
-
.clone()
|
|
192
|
-
.resolve(
|
|
193
|
-
importWithoutVite<Promise<{ render: (...args: unknown[]) => void }>>(portalEntryPoint),
|
|
194
|
-
{
|
|
195
|
-
data: {
|
|
196
|
-
name: 'bootstrap::loadPortalSourceCode',
|
|
197
|
-
level: TelemetryLevel.Debug,
|
|
198
|
-
properties: {
|
|
199
|
-
portalId,
|
|
200
|
-
portalTag,
|
|
201
|
-
entryPoint: portalEntryPoint,
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
// render the portal - this will load the portal template and render it
|
|
208
|
-
render(el, { ref, manifest: portal_manifest, config: portal_config });
|
|
209
|
-
})();
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the portal template entrypoint URL used by the SPA bootstrap loader.
|
|
3
|
-
*
|
|
4
|
-
* @param segments - Ordered URL/path segments such as proxy prefix, asset path, and template entry.
|
|
5
|
-
* @returns A normalized entrypoint string with single slashes between segments.
|
|
6
|
-
*/
|
|
7
|
-
export const createPortalEntryPoint = (...segments: Array<string | undefined | null>): string => {
|
|
8
|
-
const normalized = segments
|
|
9
|
-
// Drop undefined/null segments before further processing
|
|
10
|
-
.filter((segment): segment is string => segment !== undefined && segment !== null)
|
|
11
|
-
// Trim stray whitespace from each segment
|
|
12
|
-
.map((segment) => segment.trim())
|
|
13
|
-
// Drop any segments that became empty after trimming
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
// Strip leading slashes from every segment (and trailing slashes from the first)
|
|
16
|
-
.map((segment, index) => {
|
|
17
|
-
// The first segment may have a trailing slash (e.g. a base URL) that needs stripping
|
|
18
|
-
if (index === 0) {
|
|
19
|
-
return segment.replace(/\/+$/g, '');
|
|
20
|
-
}
|
|
21
|
-
return segment.replace(/^\/+|\/+$/g, '');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return normalized.join('/');
|
|
25
|
-
};
|
package/src/html/html.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { version } from '../version.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Represents an HTML template string used for generating the main structure of an SPA (Single Page Application).
|
|
5
|
-
*
|
|
6
|
-
* @see {@link https://vite.dev/guide/env-and-mode.html#html-constant-replacement}
|
|
7
|
-
*
|
|
8
|
-
* The template includes placeholders for dynamic values such as:
|
|
9
|
-
* - `%FUSION_SPA_TITLE%`: The title of the SPA.
|
|
10
|
-
* - `%MODE%`: The mode of the application (e.g., development, production).
|
|
11
|
-
* - `%FUSION_SPA_BOOTSTRAP%`: The path to the bootstrap script for initializing the SPA.
|
|
12
|
-
*
|
|
13
|
-
* Additionally, it includes:
|
|
14
|
-
* - A meta tag for the plugin version, dynamically populated using the `version` variable.
|
|
15
|
-
* - A link to the Equinor font stylesheet hosted on a CDN.
|
|
16
|
-
*
|
|
17
|
-
* @constant
|
|
18
|
-
* @type {string}
|
|
19
|
-
*/
|
|
20
|
-
export const html = `
|
|
21
|
-
<!DOCTYPE html>
|
|
22
|
-
<html>
|
|
23
|
-
<head>
|
|
24
|
-
<title>%FUSION_SPA_TITLE%</title>
|
|
25
|
-
<meta name="mode" content="%MODE%">
|
|
26
|
-
<meta name="fusion-spa-plugin-version" content="${version}">
|
|
27
|
-
<link rel="stylesheet" href="https://cdn.eds.equinor.com/font/equinor-font.css" />
|
|
28
|
-
<script type="module" src="%FUSION_SPA_BOOTSTRAP%"></script>
|
|
29
|
-
<script>
|
|
30
|
-
// Set AG Grid license key globally if provided
|
|
31
|
-
window.FUSION_AG_GRID_KEY = '%FUSION_SPA_AG_GRID_KEY%';
|
|
32
|
-
|
|
33
|
-
// suppress console error for custom elements already defined.
|
|
34
|
-
// WebComponents should be added by the portal, but not removed from application
|
|
35
|
-
const _customElementsDefine = window.customElements.define;
|
|
36
|
-
window.customElements.define = (name, cl, conf) => {
|
|
37
|
-
if (!customElements.get(name)) {
|
|
38
|
-
_customElementsDefine.call(window.customElements, name, cl, conf);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
</script>
|
|
42
|
-
<style>
|
|
43
|
-
html, body {
|
|
44
|
-
margin: 0;
|
|
45
|
-
padding: 0;
|
|
46
|
-
height: 100%;
|
|
47
|
-
font-family: 'EquinorFont', sans-serif;
|
|
48
|
-
}
|
|
49
|
-
</style>
|
|
50
|
-
</head>
|
|
51
|
-
<body></body>
|
|
52
|
-
</html>
|
|
53
|
-
`;
|
|
54
|
-
|
|
55
|
-
export default html;
|
package/src/html/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public API surface of the `@equinor/fusion-framework-vite-plugin-spa/html`
|
|
3
|
-
* sub-path export.
|
|
4
|
-
*
|
|
5
|
-
* @remarks
|
|
6
|
-
* Re-exports the {@link registerServiceWorker} function so that custom
|
|
7
|
-
* bootstrap files can register the SPA service worker without importing
|
|
8
|
-
* internal paths.
|
|
9
|
-
*/
|
|
10
|
-
export { registerServiceWorker } from './register-service-worker.js';
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves a Vite environment toggle that may arrive as a parsed boolean or raw string.
|
|
3
|
-
*
|
|
4
|
-
* @param value - Environment value supplied through plugin configuration or a custom Vite define.
|
|
5
|
-
* @returns `true` only for the boolean `true` or the string `'true'`.
|
|
6
|
-
*/
|
|
7
|
-
export function isEnabledEnvValue(value: unknown): boolean {
|
|
8
|
-
return value === true || value === 'true';
|
|
9
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import type { ModulesInstance } from '@equinor/fusion-framework-module';
|
|
2
|
-
import type { MsalModule } from '@equinor/fusion-framework-module-msal';
|
|
3
|
-
import { TelemetryLevel, type TelemetryModule } from '@equinor/fusion-framework-module-telemetry';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Registers the Fusion SPA service worker and wires token acquisition.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* The service worker intercepts outgoing fetch requests that match
|
|
10
|
-
* the configured {@link ResourceConfiguration | resource patterns},
|
|
11
|
-
* rewrites URLs, and injects Bearer tokens obtained from the MSAL
|
|
12
|
-
* module. This function:
|
|
13
|
-
*
|
|
14
|
-
* 1. Registers `/@fusion-spa-sw.js` as a module service worker.
|
|
15
|
-
* 2. Listens for `GET_TOKEN` messages from the worker and responds
|
|
16
|
-
* with MSAL access tokens.
|
|
17
|
-
* 3. Sends the `INIT_CONFIG` message containing resource configurations
|
|
18
|
-
* to the active worker once it is ready and controlling the page.
|
|
19
|
-
*
|
|
20
|
-
* @param framework - An initialized Fusion Framework instance that
|
|
21
|
-
* includes the {@link MsalModule} (for token acquisition) and
|
|
22
|
-
* {@link TelemetryModule} (for structured logging).
|
|
23
|
-
* @throws {Error} When service workers are not supported by the browser.
|
|
24
|
-
* @throws {Error} When the `FUSION_SPA_SERVICE_WORKER_RESOURCES`
|
|
25
|
-
* environment variable is not defined.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* import { registerServiceWorker } from '@equinor/fusion-framework-vite-plugin-spa/html';
|
|
30
|
-
*
|
|
31
|
-
* const framework = await configurator.initialize();
|
|
32
|
-
* await registerServiceWorker(framework);
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export async function registerServiceWorker(
|
|
36
|
-
framework: ModulesInstance<[MsalModule, TelemetryModule]>,
|
|
37
|
-
) {
|
|
38
|
-
const telemetry = framework.telemetry;
|
|
39
|
-
// Bail out early when the browser has no service worker support at all
|
|
40
|
-
if ('serviceWorker' in navigator === false) {
|
|
41
|
-
const exception = new Error('Service workers are not supported in this browser.');
|
|
42
|
-
exception.name = 'ServiceWorkerNotSupported';
|
|
43
|
-
telemetry.trackException({
|
|
44
|
-
name: `registerServiceWorker.${exception.name}`,
|
|
45
|
-
exception,
|
|
46
|
-
});
|
|
47
|
-
throw exception;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const resourceConfigs = import.meta.env.FUSION_SPA_SERVICE_WORKER_RESOURCES;
|
|
51
|
-
// The worker needs a resource config to know which requests to intercept
|
|
52
|
-
if (!resourceConfigs) {
|
|
53
|
-
const exception = new Error('Service worker config is not defined.');
|
|
54
|
-
exception.name = 'ServiceWorkerConfigNotDefined';
|
|
55
|
-
telemetry.trackException({
|
|
56
|
-
name: `registerServiceWorker.${exception.name}`,
|
|
57
|
-
exception,
|
|
58
|
-
});
|
|
59
|
-
throw exception;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Helper function to send configuration to the service worker
|
|
64
|
-
*/
|
|
65
|
-
const sendConfigToServiceWorker = (worker: ServiceWorker) => {
|
|
66
|
-
worker.postMessage({
|
|
67
|
-
type: 'INIT_CONFIG',
|
|
68
|
-
config: resourceConfigs,
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
// allow the service worker to start receiving messages early
|
|
74
|
-
navigator.serviceWorker.startMessages();
|
|
75
|
-
|
|
76
|
-
// listen for messages from the service worker (set up before registration)
|
|
77
|
-
navigator.serviceWorker.addEventListener('message', async (event) => {
|
|
78
|
-
// Only the GET_TOKEN message type requests a token from this handler
|
|
79
|
-
if (event.data.type === 'GET_TOKEN') {
|
|
80
|
-
try {
|
|
81
|
-
// extract scopes from the event data
|
|
82
|
-
const scopes = event.data.scopes as string[];
|
|
83
|
-
// Scopes must be a real array before they can be used to request a token
|
|
84
|
-
if (!scopes || !Array.isArray(scopes)) {
|
|
85
|
-
const error = new Error('Invalid scopes provided');
|
|
86
|
-
error.name = 'InvalidScopesProvided';
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// request a token from the MSAL module
|
|
91
|
-
const token = await framework.auth.acquireToken({ request: { scopes } });
|
|
92
|
-
|
|
93
|
-
// A missing token means acquisition failed and the worker can't proceed
|
|
94
|
-
if (!token) {
|
|
95
|
-
const error = new Error('Failed to acquire token');
|
|
96
|
-
error.name = 'FailedToAcquireToken';
|
|
97
|
-
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// send the token back to the service worker
|
|
102
|
-
event.ports[0].postMessage({
|
|
103
|
-
accessToken: token.accessToken,
|
|
104
|
-
expiresOn: token.expiresOn?.getTime(),
|
|
105
|
-
});
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const exception = error as Error;
|
|
108
|
-
telemetry.trackException({
|
|
109
|
-
name: `serviceWorker.onMessage.${exception.name}`,
|
|
110
|
-
exception,
|
|
111
|
-
});
|
|
112
|
-
event.ports[0].postMessage({
|
|
113
|
-
error: (error as Error).message,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// register the service worker with telemetry
|
|
120
|
-
// updateViaCache: 'none' ensures the service worker script is always fetched fresh
|
|
121
|
-
// This is important during development to pick up code changes
|
|
122
|
-
using measurement = telemetry.measure({
|
|
123
|
-
name: 'registerServiceWorker',
|
|
124
|
-
level: TelemetryLevel.Information,
|
|
125
|
-
});
|
|
126
|
-
const registration = await measurement.clone().resolve(
|
|
127
|
-
navigator.serviceWorker.register('/@fusion-spa-sw.js', {
|
|
128
|
-
type: 'module',
|
|
129
|
-
scope: '/',
|
|
130
|
-
updateViaCache: 'none',
|
|
131
|
-
}),
|
|
132
|
-
{
|
|
133
|
-
data: {
|
|
134
|
-
name: 'registerServiceWorker.register',
|
|
135
|
-
level: TelemetryLevel.Debug,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
// Handle service worker updates/installations
|
|
141
|
-
// If there's a service worker waiting or installing, send config when it activates
|
|
142
|
-
if (registration.waiting) {
|
|
143
|
-
sendConfigToServiceWorker(registration.waiting);
|
|
144
|
-
}
|
|
145
|
-
// A worker still installing needs to reach the 'activated' state before it can receive config
|
|
146
|
-
if (registration.installing) {
|
|
147
|
-
registration.installing.addEventListener('statechange', (event) => {
|
|
148
|
-
const worker = event.target as ServiceWorker;
|
|
149
|
-
// Only send config once the worker has fully activated
|
|
150
|
-
if (worker.state === 'activated') {
|
|
151
|
-
sendConfigToServiceWorker(worker);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Listen for controller changes (happens during hard refresh or updates)
|
|
157
|
-
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
158
|
-
// Only send config once this page is actually controlled by a worker
|
|
159
|
-
if (navigator.serviceWorker.controller) {
|
|
160
|
-
sendConfigToServiceWorker(navigator.serviceWorker.controller);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// wait for the service worker to be ready
|
|
165
|
-
const readyRegistration = await measurement.clone().resolve(navigator.serviceWorker.ready, {
|
|
166
|
-
data: {
|
|
167
|
-
name: 'registerServiceWorker.ready',
|
|
168
|
-
level: TelemetryLevel.Debug,
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// ensure we have an active service worker before sending config
|
|
173
|
-
const activeWorker = readyRegistration.active;
|
|
174
|
-
// Without an active worker there's nothing to send config to
|
|
175
|
-
if (!activeWorker) {
|
|
176
|
-
console.error('[Service Worker Registration] Service worker is not active after ready state');
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// CRITICAL: Wait for the service worker to become the controller
|
|
181
|
-
// This ensures the service worker can intercept fetch requests
|
|
182
|
-
if (!navigator.serviceWorker.controller) {
|
|
183
|
-
await measurement.clone().resolve(
|
|
184
|
-
new Promise<void>((resolve) => {
|
|
185
|
-
let checkInterval: NodeJS.Timeout;
|
|
186
|
-
|
|
187
|
-
const finish = () => {
|
|
188
|
-
clearInterval(checkInterval);
|
|
189
|
-
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
|
|
190
|
-
resolve();
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const onControllerChange = () => finish();
|
|
194
|
-
|
|
195
|
-
// If controllerchange fires, the service worker has taken control
|
|
196
|
-
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
|
|
197
|
-
|
|
198
|
-
// Polling fallback and timeout to prevent infinite waiting
|
|
199
|
-
checkInterval = setInterval(() => {
|
|
200
|
-
// Stop polling once a controller has taken over
|
|
201
|
-
if (navigator.serviceWorker.controller) finish();
|
|
202
|
-
}, 200);
|
|
203
|
-
|
|
204
|
-
setTimeout(finish, 5000);
|
|
205
|
-
}),
|
|
206
|
-
{
|
|
207
|
-
data: {
|
|
208
|
-
name: 'registerServiceWorker.controllerWait',
|
|
209
|
-
level: TelemetryLevel.Debug,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// send the config to the active service worker
|
|
216
|
-
sendConfigToServiceWorker(activeWorker);
|
|
217
|
-
|
|
218
|
-
// Also send to the controller if it exists and is different from active
|
|
219
|
-
if (navigator.serviceWorker.controller && navigator.serviceWorker.controller !== activeWorker) {
|
|
220
|
-
sendConfigToServiceWorker(navigator.serviceWorker.controller);
|
|
221
|
-
}
|
|
222
|
-
} catch (error) {
|
|
223
|
-
telemetry.trackException({
|
|
224
|
-
name: `registerServiceWorker.${(error as Error).name}`,
|
|
225
|
-
exception: error as Error,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|