@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.
- package/CHANGELOG.md +28 -0
- package/dist/esm/html/bootstrap.js +3 -2
- package/dist/esm/html/bootstrap.js.map +1 -1
- package/dist/esm/html/create-portal-entry-point.js +5 -0
- package/dist/esm/html/create-portal-entry-point.js.map +1 -1
- package/dist/esm/html/{index.html.js → html.js} +1 -1
- package/dist/esm/html/html.js.map +1 -0
- package/dist/esm/html/register-service-worker.js +10 -0
- package/dist/esm/html/register-service-worker.js.map +1 -1
- package/dist/esm/html/sw.js +6 -0
- package/dist/esm/html/sw.js.map +1 -1
- package/dist/esm/plugin.js +4 -2
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/util/{load-env.js → load-environment.js} +1 -1
- package/dist/esm/util/load-environment.js.map +1 -0
- package/dist/esm/util/object-to-env.js +2 -0
- package/dist/esm/util/object-to-env.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/html/bootstrap.js +723 -219
- package/dist/html/bootstrap.js.map +1 -1
- package/dist/html/sw.js +6 -0
- package/dist/html/sw.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/html/{index.html.d.ts → html.d.ts} +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +7 -7
- package/src/html/bootstrap.ts +4 -3
- package/src/html/create-portal-entry-point.ts +5 -0
- package/src/html/register-service-worker.ts +10 -0
- package/src/html/sw.ts +6 -0
- package/src/plugin.ts +4 -2
- package/src/util/object-to-env.ts +2 -0
- package/src/version.ts +1 -1
- package/dist/esm/html/index.html.js.map +0 -1
- package/dist/esm/util/load-env.js.map +0 -1
- /package/dist/types/util/{load-env.d.ts → load-environment.d.ts} +0 -0
- /package/src/html/{index.html.ts → html.ts} +0 -0
- /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.
|
|
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;
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "4.0.
|
|
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.
|
|
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
|
|
41
|
-
"@equinor/fusion-framework-module": "
|
|
42
|
-
"@equinor/fusion-framework-module-
|
|
43
|
-
"@equinor/fusion-framework-module-
|
|
44
|
-
"@equinor/fusion-framework-module-
|
|
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": "^
|
|
51
|
+
"typescript": "^7.0.2",
|
|
52
52
|
"vite": "^8.0.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
package/src/html/bootstrap.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { registerServiceWorker } from './register-service-worker.js';
|
|
|
19
19
|
|
|
20
20
|
import { version } from '../version.js';
|
|
21
21
|
|
|
22
|
-
//
|
|
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
|
-
//
|
|
174
|
-
//
|
|
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/
|
|
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-
|
|
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.
|
|
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
|
|
File without changes
|