@equinor/fusion-framework-vite-plugin-spa 4.0.13 → 4.0.14

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/esm/html/bootstrap.js +3 -2
  3. package/dist/esm/html/bootstrap.js.map +1 -1
  4. package/dist/esm/html/create-portal-entry-point.js +5 -0
  5. package/dist/esm/html/create-portal-entry-point.js.map +1 -1
  6. package/dist/esm/html/{index.html.js → html.js} +1 -1
  7. package/dist/esm/html/html.js.map +1 -0
  8. package/dist/esm/html/register-service-worker.js +10 -0
  9. package/dist/esm/html/register-service-worker.js.map +1 -1
  10. package/dist/esm/html/sw.js +6 -0
  11. package/dist/esm/html/sw.js.map +1 -1
  12. package/dist/esm/plugin.js +4 -2
  13. package/dist/esm/plugin.js.map +1 -1
  14. package/dist/esm/util/{load-env.js → load-environment.js} +1 -1
  15. package/dist/esm/util/load-environment.js.map +1 -0
  16. package/dist/esm/util/object-to-env.js +2 -0
  17. package/dist/esm/util/object-to-env.js.map +1 -1
  18. package/dist/esm/version.js +1 -1
  19. package/dist/html/bootstrap.js +723 -219
  20. package/dist/html/bootstrap.js.map +1 -1
  21. package/dist/html/sw.js +6 -0
  22. package/dist/html/sw.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/dist/types/html/{index.html.d.ts → html.d.ts} +1 -1
  25. package/dist/types/version.d.ts +1 -1
  26. package/package.json +7 -7
  27. package/src/html/bootstrap.ts +4 -3
  28. package/src/html/create-portal-entry-point.ts +5 -0
  29. package/src/html/register-service-worker.ts +10 -0
  30. package/src/html/sw.ts +6 -0
  31. package/src/plugin.ts +4 -2
  32. package/src/util/object-to-env.ts +2 -0
  33. package/src/version.ts +1 -1
  34. package/dist/esm/html/index.html.js.map +0 -1
  35. package/dist/esm/util/load-env.js.map +0 -1
  36. /package/dist/types/util/{load-env.d.ts → load-environment.d.ts} +0 -0
  37. /package/src/html/{index.html.ts → html.ts} +0 -0
  38. /package/src/util/{load-env.ts → load-environment.ts} +0 -0
@@ -15,5 +15,5 @@
15
15
  * @constant
16
16
  * @type {string}
17
17
  */
18
- export declare const html = "\n <!DOCTYPE html>\n <html>\n <head>\n <title>%FUSION_SPA_TITLE%</title>\n <meta name=\"mode\" content=\"%MODE%\">\n <meta name=\"fusion-spa-plugin-version\" content=\"4.0.13\">\n <link rel=\"stylesheet\" href=\"https://cdn.eds.equinor.com/font/equinor-font.css\" />\n <script type=\"module\" src=\"%FUSION_SPA_BOOTSTRAP%\"></script>\n <script>\n // Set AG Grid license key globally if provided\n window.FUSION_AG_GRID_KEY = '%FUSION_SPA_AG_GRID_KEY%';\n \n // suppress console error for custom elements already defined. \n // WebComponents should be added by the portal, but not removed from application\n const _customElementsDefine = window.customElements.define;\n window.customElements.define = (name, cl, conf) => {\n if (!customElements.get(name)) {\n _customElementsDefine.call(window.customElements, name, cl, conf);\n }\n };\n </script>\n <style>\n html, body {\n margin: 0;\n padding: 0;\n height: 100%;\n font-family: 'EquinorFont', sans-serif;\n }\n </style>\n </head>\n <body></body>\n </html>\n";
18
+ export declare const html = "\n <!DOCTYPE html>\n <html>\n <head>\n <title>%FUSION_SPA_TITLE%</title>\n <meta name=\"mode\" content=\"%MODE%\">\n <meta name=\"fusion-spa-plugin-version\" content=\"4.0.14\">\n <link rel=\"stylesheet\" href=\"https://cdn.eds.equinor.com/font/equinor-font.css\" />\n <script type=\"module\" src=\"%FUSION_SPA_BOOTSTRAP%\"></script>\n <script>\n // Set AG Grid license key globally if provided\n window.FUSION_AG_GRID_KEY = '%FUSION_SPA_AG_GRID_KEY%';\n \n // suppress console error for custom elements already defined. \n // WebComponents should be added by the portal, but not removed from application\n const _customElementsDefine = window.customElements.define;\n window.customElements.define = (name, cl, conf) => {\n if (!customElements.get(name)) {\n _customElementsDefine.call(window.customElements, name, cl, conf);\n }\n };\n </script>\n <style>\n html, body {\n margin: 0;\n padding: 0;\n height: 100%;\n font-family: 'EquinorFont', sans-serif;\n }\n </style>\n </head>\n <body></body>\n </html>\n";
19
19
  export default html;
@@ -1 +1 @@
1
- export declare const version = "4.0.13";
1
+ export declare const version = "4.0.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@equinor/fusion-framework-vite-plugin-spa",
3
- "version": "4.0.13",
3
+ "version": "4.0.14",
4
4
  "description": "Vite plugin for SPA development",
5
5
  "type": "module",
6
6
  "types": "dist/types/index.d.ts",
@@ -37,18 +37,18 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@equinor/fusion-framework-module-http": "8.0.3",
41
- "@equinor/fusion-framework-module": "6.1.0",
42
- "@equinor/fusion-framework-module-telemetry": "7.0.0",
43
- "@equinor/fusion-framework-module-msal": "10.0.1",
44
- "@equinor/fusion-framework-module-service-discovery": "10.0.1"
40
+ "@equinor/fusion-framework-module": "6.1.1",
41
+ "@equinor/fusion-framework-module-http": "8.0.4",
42
+ "@equinor/fusion-framework-module-service-discovery": "10.0.2",
43
+ "@equinor/fusion-framework-module-telemetry": "7.0.1",
44
+ "@equinor/fusion-framework-module-msal": "10.0.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@rollup/plugin-commonjs": "^29.0.0",
48
48
  "@rollup/plugin-node-resolve": "^16.0.1",
49
49
  "@rollup/plugin-typescript": "^12.1.2",
50
50
  "rollup": "^4.59.0",
51
- "typescript": "^6.0.3",
51
+ "typescript": "^7.0.2",
52
52
  "vite": "^8.0.0"
53
53
  },
54
54
  "scripts": {
@@ -19,7 +19,7 @@ import { registerServiceWorker } from './register-service-worker.js';
19
19
 
20
20
  import { version } from '../version.js';
21
21
 
22
- // @todo - add type for portal manifest when available
22
+ // TODO(#5065): add type for portal manifest when available
23
23
  type PortalManifest = {
24
24
  build: {
25
25
  config: Record<string, unknown>;
@@ -89,6 +89,7 @@ enableTelemetry(configurator, {
89
89
  },
90
90
  // biome-ignore lint/suspicious/noExplicitAny: we need to use any here to allow dynamic properties
91
91
  } as Record<string, any>;
92
+ // Only attach user metadata when the auth module has resolved an account
92
93
  if (modules?.auth) {
93
94
  metadata.fusion.user = {
94
95
  id: modules.auth.account?.homeAccountId,
@@ -170,8 +171,8 @@ enableTelemetry(configurator, {
170
171
  portal_manifest.build.templateEntry,
171
172
  );
172
173
 
173
- // @todo: should test if the entrypoint is external or internal
174
- // @todo: add proper return type
174
+ // TODO(#5066): should test if the entrypoint is external or internal
175
+ // TODO(#5066): add proper return type
175
176
  const { render } = await measurement
176
177
  .clone()
177
178
  .resolve(
@@ -6,10 +6,15 @@
6
6
  */
7
7
  export const createPortalEntryPoint = (...segments: Array<string | undefined | null>): string => {
8
8
  const normalized = segments
9
+ // Drop undefined/null segments before further processing
9
10
  .filter((segment): segment is string => segment !== undefined && segment !== null)
11
+ // Trim stray whitespace from each segment
10
12
  .map((segment) => segment.trim())
13
+ // Drop any segments that became empty after trimming
11
14
  .filter(Boolean)
15
+ // Strip leading slashes from every segment (and trailing slashes from the first)
12
16
  .map((segment, index) => {
17
+ // The first segment may have a trailing slash (e.g. a base URL) that needs stripping
13
18
  if (index === 0) {
14
19
  return segment.replace(/\/+$/g, '');
15
20
  }
@@ -36,6 +36,7 @@ export async function registerServiceWorker(
36
36
  framework: ModulesInstance<[MsalModule, TelemetryModule]>,
37
37
  ) {
38
38
  const telemetry = framework.telemetry;
39
+ // Bail out early when the browser has no service worker support at all
39
40
  if ('serviceWorker' in navigator === false) {
40
41
  const exception = new Error('Service workers are not supported in this browser.');
41
42
  exception.name = 'ServiceWorkerNotSupported';
@@ -47,6 +48,7 @@ export async function registerServiceWorker(
47
48
  }
48
49
 
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
50
52
  if (!resourceConfigs) {
51
53
  const exception = new Error('Service worker config is not defined.');
52
54
  exception.name = 'ServiceWorkerConfigNotDefined';
@@ -73,10 +75,12 @@ export async function registerServiceWorker(
73
75
 
74
76
  // listen for messages from the service worker (set up before registration)
75
77
  navigator.serviceWorker.addEventListener('message', async (event) => {
78
+ // Only the GET_TOKEN message type requests a token from this handler
76
79
  if (event.data.type === 'GET_TOKEN') {
77
80
  try {
78
81
  // extract scopes from the event data
79
82
  const scopes = event.data.scopes as string[];
83
+ // Scopes must be a real array before they can be used to request a token
80
84
  if (!scopes || !Array.isArray(scopes)) {
81
85
  const error = new Error('Invalid scopes provided');
82
86
  error.name = 'InvalidScopesProvided';
@@ -86,6 +90,7 @@ export async function registerServiceWorker(
86
90
  // request a token from the MSAL module
87
91
  const token = await framework.auth.acquireToken({ request: { scopes } });
88
92
 
93
+ // A missing token means acquisition failed and the worker can't proceed
89
94
  if (!token) {
90
95
  const error = new Error('Failed to acquire token');
91
96
  error.name = 'FailedToAcquireToken';
@@ -137,9 +142,11 @@ export async function registerServiceWorker(
137
142
  if (registration.waiting) {
138
143
  sendConfigToServiceWorker(registration.waiting);
139
144
  }
145
+ // A worker still installing needs to reach the 'activated' state before it can receive config
140
146
  if (registration.installing) {
141
147
  registration.installing.addEventListener('statechange', (event) => {
142
148
  const worker = event.target as ServiceWorker;
149
+ // Only send config once the worker has fully activated
143
150
  if (worker.state === 'activated') {
144
151
  sendConfigToServiceWorker(worker);
145
152
  }
@@ -148,6 +155,7 @@ export async function registerServiceWorker(
148
155
 
149
156
  // Listen for controller changes (happens during hard refresh or updates)
150
157
  navigator.serviceWorker.addEventListener('controllerchange', () => {
158
+ // Only send config once this page is actually controlled by a worker
151
159
  if (navigator.serviceWorker.controller) {
152
160
  sendConfigToServiceWorker(navigator.serviceWorker.controller);
153
161
  }
@@ -163,6 +171,7 @@ export async function registerServiceWorker(
163
171
 
164
172
  // ensure we have an active service worker before sending config
165
173
  const activeWorker = readyRegistration.active;
174
+ // Without an active worker there's nothing to send config to
166
175
  if (!activeWorker) {
167
176
  console.error('[Service Worker Registration] Service worker is not active after ready state');
168
177
  return;
@@ -188,6 +197,7 @@ export async function registerServiceWorker(
188
197
 
189
198
  // Polling fallback and timeout to prevent infinite waiting
190
199
  checkInterval = setInterval(() => {
200
+ // Stop polling once a controller has taken over
191
201
  if (navigator.serviceWorker.controller) finish();
192
202
  }, 200);
193
203
 
package/src/html/sw.ts CHANGED
@@ -65,6 +65,7 @@ function getScopeKey(scopes: string[]): string {
65
65
  */
66
66
  function isTokenValid(scopes: string[]): boolean {
67
67
  const scopeKey = getScopeKey(scopes);
68
+ // No cached entry at all means there's nothing to validate
68
69
  if (!tokenCache.has(scopeKey)) {
69
70
  return false;
70
71
  }
@@ -99,6 +100,7 @@ async function requestTokenFromClient(scopes: string[]): Promise<Token> {
99
100
  const messageChannel = new MessageChannel();
100
101
  const token = await new Promise<Token>((resolve, reject) => {
101
102
  messageChannel.port1.onmessage = (event) => {
103
+ // Reject when the client reports it couldn't provide a token
102
104
  if (event.data.error) {
103
105
  reject(event.data.error);
104
106
  }
@@ -107,6 +109,7 @@ async function requestTokenFromClient(scopes: string[]): Promise<Token> {
107
109
  clients[0].postMessage({ type: 'GET_TOKEN', scopes }, [messageChannel.port2]);
108
110
  });
109
111
 
112
+ // A resolved but empty response means the client didn't actually return a token
110
113
  if (!token) {
111
114
  throw new Error('No token received');
112
115
  }
@@ -132,6 +135,7 @@ async function getToken(scopes: string[]): Promise<string> {
132
135
  }
133
136
  const scopeKey = getScopeKey(scopes);
134
137
  const { accessToken } = tokenCache.get(scopeKey) || {};
138
+ // A missing accessToken here means the client failed to provide a usable token
135
139
  if (!accessToken) {
136
140
  throw new Error('No access token found');
137
141
  }
@@ -155,6 +159,7 @@ async function getToken(scopes: string[]): Promise<string> {
155
159
  * - The comparison is performed using fully resolved absolute URLs.
156
160
  */
157
161
  function getMatchingConfig(url: string): ResourceConfiguration | undefined {
162
+ // Find the first configured resource whose resolved base URL prefixes the request URL
158
163
  return resourceConfigurations.find((config) => {
159
164
  const configUrl = new URL(
160
165
  config.url,
@@ -178,6 +183,7 @@ self.addEventListener('activate', (event: ExtendableEvent) => {
178
183
  // Handle configuration from main thread
179
184
  self.addEventListener('message', async (event: ExtendableMessageEvent) => {
180
185
  const { type, config } = event.data;
186
+ // Only the INIT_CONFIG message carries resource configuration to apply
181
187
  if (type === 'INIT_CONFIG') {
182
188
  resourceConfigurations = config as ResourceConfiguration[];
183
189
 
package/src/plugin.ts CHANGED
@@ -2,10 +2,10 @@ import { normalizePath, type Plugin } from 'vite';
2
2
 
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
- import defaultTemplate from './html/index.html.js';
5
+ import defaultTemplate from './html/html.js';
6
6
 
7
7
  import { objectToEnv } from './util/object-to-env.js';
8
- import { loadEnvironment } from './util/load-env.js';
8
+ import { loadEnvironment } from './util/load-environment.js';
9
9
 
10
10
  import type { TemplateEnv, TemplateEnvFn } from './types.js';
11
11
 
@@ -144,12 +144,14 @@ export const plugin = <TEnv extends TemplateEnv = TemplateEnv>(
144
144
 
145
145
  log?.debug('plugin loaded environment\n', pluginEnv);
146
146
 
147
+ // Loaded env values override plugin-configured defaults
147
148
  const env = { ...pluginEnv, ...loadedEnv };
148
149
 
149
150
  log?.debug('plugin environment\n', env);
150
151
 
151
152
  // define environment variables
152
153
  config.define ??= {};
154
+ // Convert each env entry into a valid JS expression for Vite's config.define
153
155
  for (const [key, value] of Object.entries(env)) {
154
156
  // All values must be valid JavaScript expressions for Vite's config.define
155
157
  // Try to parse as JSON first (handles booleans, numbers, objects, arrays)
@@ -24,12 +24,14 @@
24
24
  * ```
25
25
  */
26
26
  export function objectToEnv(obj: object, prefix = 'FUSION_SPA'): Record<string, string> {
27
+ // Build up the flat env record one entry at a time, recursing into nested objects
27
28
  return Object.entries(obj).reduce((result, [key, value]) => {
28
29
  // Convert camelCase to snake_case and uppercase
29
30
  const snakeKey = key.replace(/([A-Z])/g, '_$1').toUpperCase();
30
31
 
31
32
  const newPrefix = prefix ? `${prefix.replace(/_$/, '')}_${snakeKey}` : snakeKey;
32
33
 
34
+ // Nested (non-array) objects are flattened recursively under the extended prefix
33
35
  if (value && typeof value === 'object' && !Array.isArray(value)) {
34
36
  // Recursively flatten nested objects
35
37
  return Object.assign(result, objectToEnv(value, newPrefix));
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '4.0.13';
2
+ export const version = '4.0.14';
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.html.js","sourceRoot":"","sources":["../../../src/html/index.html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;wDAMoC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"load-env.js","sourceRoot":"","sources":["../../../src/util/load-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,OAAO,EAAmB,MAAM,MAAM,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAkB,EAClB,GAAc,EACd,SAAS,GAAG,aAAa;IAEzB,6BAA6B;IAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,oCAAoC;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;IACnF,0DAA0D;IAC1D,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,eAAe,eAAe,CAAC"}
File without changes
File without changes