@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/src/html/sw.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/// <reference lib="webworker" />
|
|
2
|
-
|
|
3
|
-
import type { ResourceConfiguration } from '../types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Represents an authentication token with an access token and its expiration time.
|
|
7
|
-
*/
|
|
8
|
-
type Token = {
|
|
9
|
-
accessToken: string;
|
|
10
|
-
expiresOn: number;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A cache structure for storing tokens, where each token is associated with a unique string key.
|
|
15
|
-
*
|
|
16
|
-
* @typeParam string - The key used to identify a token in the cache.
|
|
17
|
-
* @typeParam Token - The type of the token being stored in the cache.
|
|
18
|
-
*/
|
|
19
|
-
type TokenCache = Map<string, Token>;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A reference to the global scope of the service worker.
|
|
23
|
-
*
|
|
24
|
-
* The `self` variable is explicitly cast to `ServiceWorkerGlobalScope` to ensure
|
|
25
|
-
* type safety and provide access to service worker-specific APIs.
|
|
26
|
-
*
|
|
27
|
-
* This is necessary because `globalThis` is a generic global object and does not
|
|
28
|
-
* include service worker-specific properties and methods by default.
|
|
29
|
-
*/
|
|
30
|
-
const self = globalThis as unknown as ServiceWorkerGlobalScope;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* An array of settings used for token injection.
|
|
34
|
-
* Each setting defines the configuration for injecting tokens
|
|
35
|
-
* into the application, such as authentication or API tokens.
|
|
36
|
-
*/
|
|
37
|
-
let resourceConfigurations: ResourceConfiguration[] = [];
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* A cache for storing tokens, implemented as a `Map`.
|
|
41
|
-
* This cache is used to temporarily hold tokens for quick retrieval.
|
|
42
|
-
*
|
|
43
|
-
* @type {TokenCache} - A `Map` instance where the keys and values are determined by the `TokenCache` type definition.
|
|
44
|
-
*/
|
|
45
|
-
const tokenCache: TokenCache = new Map();
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Generates a unique key by sorting and concatenating an array of scope strings.
|
|
49
|
-
*
|
|
50
|
-
* @param scopes - An array of strings representing the scopes to be processed.
|
|
51
|
-
* @returns A single string representing the sorted and concatenated scopes, separated by commas.
|
|
52
|
-
*/
|
|
53
|
-
function getScopeKey(scopes: string[]): string {
|
|
54
|
-
return scopes.sort().join(',');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Checks if a token associated with the specified scopes is valid.
|
|
59
|
-
*
|
|
60
|
-
* This function determines the validity of a token by checking if it exists
|
|
61
|
-
* in the token cache and if its expiration time has not been reached.
|
|
62
|
-
*
|
|
63
|
-
* @param scopes - An array of strings representing the scopes for which the token is required.
|
|
64
|
-
* @returns `true` if a valid token exists for the given scopes; otherwise, `false`.
|
|
65
|
-
*/
|
|
66
|
-
function isTokenValid(scopes: string[]): boolean {
|
|
67
|
-
const scopeKey = getScopeKey(scopes);
|
|
68
|
-
// No cached entry at all means there's nothing to validate
|
|
69
|
-
if (!tokenCache.has(scopeKey)) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
const tokenData = tokenCache.get(scopeKey);
|
|
73
|
-
return tokenData !== undefined && Date.now() < tokenData.expiresOn;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Requests an access token from a client using the Service Worker's `clients` API.
|
|
78
|
-
* Communicates with the client via a `MessageChannel` to retrieve the token.
|
|
79
|
-
*
|
|
80
|
-
* @param scopes - An array of strings representing the scopes for which the token is requested.
|
|
81
|
-
* @returns A promise that resolves to the token object containing the `accessToken` and `expiresOn` properties.
|
|
82
|
-
* @throws An error if no clients are available or if the client responds with an error.
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```typescript
|
|
86
|
-
* const token = await requestTokenFromClient(['scope1', 'scope2']);
|
|
87
|
-
* console.log(token.accessToken); // Access token string
|
|
88
|
-
* console.log(token.expiresOn); // Expiration timestamp
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
async function requestTokenFromClient(scopes: string[]): Promise<Token> {
|
|
92
|
-
const clients = await self.clients.matchAll();
|
|
93
|
-
|
|
94
|
-
// ensure there are clients available
|
|
95
|
-
if (clients.length === 0) {
|
|
96
|
-
throw new Error('No clients available');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// create a message channel to communicate with the client
|
|
100
|
-
const messageChannel = new MessageChannel();
|
|
101
|
-
const token = await new Promise<Token>((resolve, reject) => {
|
|
102
|
-
messageChannel.port1.onmessage = (event) => {
|
|
103
|
-
// Reject when the client reports it couldn't provide a token
|
|
104
|
-
if (event.data.error) {
|
|
105
|
-
reject(event.data.error);
|
|
106
|
-
}
|
|
107
|
-
resolve(event.data as { accessToken: string; expiresOn: number });
|
|
108
|
-
};
|
|
109
|
-
clients[0].postMessage({ type: 'GET_TOKEN', scopes }, [messageChannel.port2]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// A resolved but empty response means the client didn't actually return a token
|
|
113
|
-
if (!token) {
|
|
114
|
-
throw new Error('No token received');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// store the token in the cache
|
|
118
|
-
tokenCache.set(getScopeKey(scopes), token);
|
|
119
|
-
|
|
120
|
-
return token;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Retrieves an access token for the specified scopes. If no valid token is found,
|
|
125
|
-
* it requests a new one from the client.
|
|
126
|
-
*
|
|
127
|
-
* @param scopes - An array of strings representing the required scopes for the token.
|
|
128
|
-
* @returns A promise that resolves to the access token as a string.
|
|
129
|
-
* @throws An error if no access token is found after attempting to retrieve or request one.
|
|
130
|
-
*/
|
|
131
|
-
async function getToken(scopes: string[]): Promise<string> {
|
|
132
|
-
// if no valid token is found, request a new one
|
|
133
|
-
if (!isTokenValid(scopes)) {
|
|
134
|
-
await requestTokenFromClient(scopes);
|
|
135
|
-
}
|
|
136
|
-
const scopeKey = getScopeKey(scopes);
|
|
137
|
-
const { accessToken } = tokenCache.get(scopeKey) || {};
|
|
138
|
-
// A missing accessToken here means the client failed to provide a usable token
|
|
139
|
-
if (!accessToken) {
|
|
140
|
-
throw new Error('No access token found');
|
|
141
|
-
}
|
|
142
|
-
return accessToken;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Match request to proxy config
|
|
146
|
-
/**
|
|
147
|
-
* Retrieves the matching token injection configuration for a given URL.
|
|
148
|
-
*
|
|
149
|
-
* @param url - The URL to match against the token injection settings.
|
|
150
|
-
* @returns The matching `TokenInjectionSetting` if found, otherwise `undefined`.
|
|
151
|
-
*
|
|
152
|
-
* The function compares the provided URL with the `url` property of each
|
|
153
|
-
* `TokenInjectionSetting` in the `tokenInjectionSettings` array. If the
|
|
154
|
-
* provided URL starts with the resolved `config.url`, it is considered a match.
|
|
155
|
-
*
|
|
156
|
-
* Note:
|
|
157
|
-
* - If `config.url` starts with a `/`, it is resolved relative to the service
|
|
158
|
-
* worker's origin (`self.location.origin`).
|
|
159
|
-
* - The comparison is performed using fully resolved absolute URLs.
|
|
160
|
-
*/
|
|
161
|
-
function getMatchingConfig(url: string): ResourceConfiguration | undefined {
|
|
162
|
-
// Find the first configured resource whose resolved base URL prefixes the request URL
|
|
163
|
-
return resourceConfigurations.find((config) => {
|
|
164
|
-
const configUrl = new URL(
|
|
165
|
-
config.url,
|
|
166
|
-
config.url.startsWith('/') ? self.location.origin : undefined,
|
|
167
|
-
).href;
|
|
168
|
-
const requestUrl = new URL(url, self.location.origin).href;
|
|
169
|
-
return requestUrl.startsWith(configUrl);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Install event
|
|
174
|
-
self.addEventListener('install', (event: ExtendableEvent) => {
|
|
175
|
-
event.waitUntil(self.skipWaiting());
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Activate event
|
|
179
|
-
self.addEventListener('activate', (event: ExtendableEvent) => {
|
|
180
|
-
event.waitUntil(self.clients.claim());
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Handle configuration from main thread
|
|
184
|
-
self.addEventListener('message', async (event: ExtendableMessageEvent) => {
|
|
185
|
-
const { type, config } = event.data;
|
|
186
|
-
// Only the INIT_CONFIG message carries resource configuration to apply
|
|
187
|
-
if (type === 'INIT_CONFIG') {
|
|
188
|
-
resourceConfigurations = config as ResourceConfiguration[];
|
|
189
|
-
|
|
190
|
-
// CRITICAL: Force skipWaiting() and claim clients to ensure this service worker takes control
|
|
191
|
-
// This handles both waiting and already-active service workers during hard refresh
|
|
192
|
-
// - skipWaiting() forces activation if the service worker is in waiting state
|
|
193
|
-
// - clients.claim() takes control of all clients immediately
|
|
194
|
-
await self.skipWaiting();
|
|
195
|
-
await self.clients.claim();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Handle fetch events
|
|
200
|
-
self.addEventListener('fetch', (event: FetchEvent) => {
|
|
201
|
-
const request = event.request.clone();
|
|
202
|
-
const url = new URL(request.url);
|
|
203
|
-
const matchedConfig = getMatchingConfig(url.toString());
|
|
204
|
-
|
|
205
|
-
// only handle requests that match the config
|
|
206
|
-
if (matchedConfig) {
|
|
207
|
-
const requestHeaders = new Headers(request.headers);
|
|
208
|
-
const handleRequest = async () => {
|
|
209
|
-
// if the matched config has scopes, append the token to the request
|
|
210
|
-
if (matchedConfig.scopes) {
|
|
211
|
-
const token = await getToken(matchedConfig.scopes);
|
|
212
|
-
requestHeaders.set('Authorization', `Bearer ${token}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// if the matched config has a rewrite, rewrite the url
|
|
216
|
-
if (typeof matchedConfig.rewrite === 'string') {
|
|
217
|
-
url.pathname = url.pathname.replace(matchedConfig?.url, matchedConfig.rewrite);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Consume the ReadableStream body and convert to text
|
|
221
|
-
// ReadableStreams can only be consumed once, so we extract the content here
|
|
222
|
-
// request.text() resolves to empty string when the request has no body
|
|
223
|
-
const body = await request.text();
|
|
224
|
-
|
|
225
|
-
// fetch the request with the modified url and headers, preserving the original HTTP method and body
|
|
226
|
-
// This ensures OPTIONS, PATCH, DELETE and other methods are forwarded correctly
|
|
227
|
-
// `cache` is forwarded explicitly - otherwise callers requesting `no-store` (e.g. polling
|
|
228
|
-
// endpoints) would silently fall back to the default HTTP cache once re-fetched here.
|
|
229
|
-
return fetch(url, {
|
|
230
|
-
method: request.method,
|
|
231
|
-
headers: requestHeaders,
|
|
232
|
-
body: body || undefined,
|
|
233
|
-
cache: request.cache,
|
|
234
|
-
});
|
|
235
|
-
};
|
|
236
|
-
event.respondWith(handleRequest());
|
|
237
|
-
}
|
|
238
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module @equinor/fusion-framework-vite-plugin-spa
|
|
3
|
-
*
|
|
4
|
-
* Vite plugin for building Fusion Framework Single Page Applications (SPAs).
|
|
5
|
-
*
|
|
6
|
-
* Provides HTML template generation, MSAL authentication bootstrapping,
|
|
7
|
-
* service discovery wiring, portal loading, and authenticated API proxying
|
|
8
|
-
* via a service worker.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* This plugin is intended for non-production development environments and
|
|
12
|
-
* is designed for use with `@equinor/fusion-framework-cli`.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* import { fusionSpaPlugin } from '@equinor/fusion-framework-vite-plugin-spa';
|
|
17
|
-
*
|
|
18
|
-
* export default defineConfig({
|
|
19
|
-
* plugins: [
|
|
20
|
-
* fusionSpaPlugin({
|
|
21
|
-
* generateTemplateEnv: () => ({
|
|
22
|
-
* title: 'My App',
|
|
23
|
-
* portal: { id: 'my-portal' },
|
|
24
|
-
* serviceDiscovery: { url: 'https://...', scopes: ['api://...'] },
|
|
25
|
-
* msal: { tenantId: '...', clientId: '...', redirectUri: '...' },
|
|
26
|
-
* }),
|
|
27
|
-
* }),
|
|
28
|
-
* ],
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export { default, plugin as fusionSpaPlugin, type PluginOptions } from './plugin.js';
|
|
33
|
-
|
|
34
|
-
export * from './types.js';
|
package/src/plugin.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { normalizePath, type Plugin } from 'vite';
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
import defaultTemplate from './html/html.js';
|
|
6
|
-
|
|
7
|
-
import { objectToEnv } from './util/object-to-env.js';
|
|
8
|
-
import { loadEnvironment } from './util/load-environment.js';
|
|
9
|
-
|
|
10
|
-
import type { TemplateEnv, TemplateEnvFn } from './types.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Options accepted by the Fusion SPA Vite plugin.
|
|
14
|
-
*
|
|
15
|
-
* @remarks
|
|
16
|
-
* Controls HTML template generation, environment variable prefixing,
|
|
17
|
-
* and the factory that produces template environment values from the
|
|
18
|
-
* current Vite build/serve context.
|
|
19
|
-
*
|
|
20
|
-
* @template TEnv - Shape of the template environment. Defaults to {@link TemplateEnv}.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* const opts: PluginOptions<FusionTemplateEnv> = {
|
|
25
|
-
* templateEnvPrefix: 'FUSION_SPA_',
|
|
26
|
-
* generateTemplateEnv: (env) => ({
|
|
27
|
-
* title: 'My App',
|
|
28
|
-
* portal: { id: 'my-portal' },
|
|
29
|
-
* serviceDiscovery: { url: '...', scopes: ['...'] },
|
|
30
|
-
* msal: { tenantId: '...', clientId: '...', redirectUri: '...' },
|
|
31
|
-
* }),
|
|
32
|
-
* };
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export type PluginOptions<TEnv extends TemplateEnv = TemplateEnv> = {
|
|
36
|
-
/**
|
|
37
|
-
* Custom HTML template string.
|
|
38
|
-
*
|
|
39
|
-
* @remarks
|
|
40
|
-
* When omitted the plugin uses a built-in template that loads the
|
|
41
|
-
* bootstrap script, sets the page title, and includes the Equinor font.
|
|
42
|
-
*/
|
|
43
|
-
template?: string;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Prefix used when reading environment variables from `.env` files.
|
|
47
|
-
*
|
|
48
|
-
* @defaultValue `'FUSION_SPA_'`
|
|
49
|
-
*/
|
|
50
|
-
templateEnvPrefix?: string;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Factory that returns partial environment values merged with defaults
|
|
54
|
-
* and flattened into `{templateEnvPrefix}*` variables.
|
|
55
|
-
*
|
|
56
|
-
* @see {@link TemplateEnvFn}
|
|
57
|
-
*/
|
|
58
|
-
generateTemplateEnv?: TemplateEnvFn<TEnv>;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Optional logger used for plugin diagnostics during Vite config
|
|
62
|
-
* resolution.
|
|
63
|
-
*/
|
|
64
|
-
logger?: Pick<Console, 'debug' | 'info' | 'warn' | 'error'>;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Built-in defaults applied when no matching value is provided by
|
|
69
|
-
* {@link PluginOptions.generateTemplateEnv} or `.env` files.
|
|
70
|
-
*/
|
|
71
|
-
const defaultEnv: Partial<TemplateEnv> = {
|
|
72
|
-
title: 'Fusion SPA',
|
|
73
|
-
bootstrap: '/@fusion-spa-bootstrap.js',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Creates the Fusion SPA Vite plugin.
|
|
78
|
-
*
|
|
79
|
-
* @remarks
|
|
80
|
-
* The plugin hooks into Vite's `config`, `resolveId`, and `configureServer`
|
|
81
|
-
* lifecycle to:
|
|
82
|
-
*
|
|
83
|
-
* 1. Flatten {@link PluginOptions.generateTemplateEnv | template env} values
|
|
84
|
-
* and `.env` overrides into `import.meta.env.FUSION_SPA_*` defines.
|
|
85
|
-
* 2. Resolve virtual module IDs (`/@fusion-spa-bootstrap.js`,
|
|
86
|
-
* `/@fusion-spa-sw.js`) to the package's pre-built HTML assets.
|
|
87
|
-
* 3. Serve the SPA HTML template for every `text/html` GET request
|
|
88
|
-
* (SPA fallback).
|
|
89
|
-
*
|
|
90
|
-
* @template TEnv - Shape of the template environment.
|
|
91
|
-
* @param options - Plugin configuration. See {@link PluginOptions}.
|
|
92
|
-
* @returns A Vite {@link Plugin} instance named `fusion-framework-plugin-spa`.
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```ts
|
|
96
|
-
* import { plugin as fusionSpaPlugin } from '@equinor/fusion-framework-vite-plugin-spa';
|
|
97
|
-
*
|
|
98
|
-
* export default defineConfig({
|
|
99
|
-
* plugins: [
|
|
100
|
-
* fusionSpaPlugin({
|
|
101
|
-
* generateTemplateEnv: () => ({
|
|
102
|
-
* title: 'My App',
|
|
103
|
-
* portal: { id: 'my-portal' },
|
|
104
|
-
* }),
|
|
105
|
-
* }),
|
|
106
|
-
* ],
|
|
107
|
-
* });
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export const plugin = <TEnv extends TemplateEnv = TemplateEnv>(
|
|
111
|
-
options?: PluginOptions<TEnv>,
|
|
112
|
-
): Plugin => {
|
|
113
|
-
// SPA index template
|
|
114
|
-
const indexTemplate = options?.template ?? defaultTemplate;
|
|
115
|
-
const log = options?.logger;
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
name: 'fusion-framework-plugin-spa',
|
|
119
|
-
resolveId: async (id) => {
|
|
120
|
-
// resolve resource aliases to the correct path
|
|
121
|
-
switch (id) {
|
|
122
|
-
case '/@fusion-spa-bootstrap.js': {
|
|
123
|
-
const file = await import.meta.resolve(
|
|
124
|
-
'@equinor/fusion-framework-vite-plugin-spa/bootstrap.js',
|
|
125
|
-
);
|
|
126
|
-
return fileURLToPath(file);
|
|
127
|
-
}
|
|
128
|
-
case '/@fusion-spa-sw.js': {
|
|
129
|
-
const file = await import.meta.resolve('@equinor/fusion-framework-vite-plugin-spa/sw.js');
|
|
130
|
-
return fileURLToPath(file);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
config: async (config, configEnv) => {
|
|
135
|
-
const templateEnvPrefix = options?.templateEnvPrefix ?? 'FUSION_SPA_';
|
|
136
|
-
// generate environment variables from plugin options
|
|
137
|
-
const pluginEnvObj = { ...defaultEnv, ...options?.generateTemplateEnv?.(configEnv) };
|
|
138
|
-
const pluginEnv = objectToEnv(pluginEnvObj ?? defaultEnv, templateEnvPrefix);
|
|
139
|
-
|
|
140
|
-
log?.debug('plugin config environment\n', pluginEnv);
|
|
141
|
-
|
|
142
|
-
// load environment variables from files
|
|
143
|
-
const loadedEnv = loadEnvironment(config, configEnv, templateEnvPrefix);
|
|
144
|
-
|
|
145
|
-
log?.debug('plugin loaded environment\n', pluginEnv);
|
|
146
|
-
|
|
147
|
-
// Loaded env values override plugin-configured defaults
|
|
148
|
-
const env = { ...pluginEnv, ...loadedEnv };
|
|
149
|
-
|
|
150
|
-
log?.debug('plugin environment\n', env);
|
|
151
|
-
|
|
152
|
-
// define environment variables
|
|
153
|
-
config.define ??= {};
|
|
154
|
-
// Convert each env entry into a valid JS expression for Vite's config.define
|
|
155
|
-
for (const [key, value] of Object.entries(env)) {
|
|
156
|
-
// All values must be valid JavaScript expressions for Vite's config.define
|
|
157
|
-
// Try to parse as JSON first (handles booleans, numbers, objects, arrays)
|
|
158
|
-
// If that fails, stringify the raw value as a string literal
|
|
159
|
-
try {
|
|
160
|
-
const parsed = JSON.parse(String(value));
|
|
161
|
-
// Re-stringify to ensure it's a valid JS expression
|
|
162
|
-
config.define[`import.meta.env.${key}`] = JSON.stringify(parsed);
|
|
163
|
-
} catch {
|
|
164
|
-
// If JSON.parse fails, it's a plain string - wrap it as a JSON string for Vite
|
|
165
|
-
config.define[`import.meta.env.${key}`] = JSON.stringify(String(value));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
config.server ??= {};
|
|
170
|
-
config.server.fs ??= {};
|
|
171
|
-
config.server.fs.allow ??= [];
|
|
172
|
-
// allow access to the html directory
|
|
173
|
-
|
|
174
|
-
const htmlDir = fileURLToPath(new URL('../html', import.meta.url));
|
|
175
|
-
config.server.fs.allow.push(normalizePath(htmlDir));
|
|
176
|
-
|
|
177
|
-
log?.info(`plugin configured for ${env.FUSION_SPA_PORTAL_ID}`);
|
|
178
|
-
},
|
|
179
|
-
configureServer(server) {
|
|
180
|
-
// Apply SPA fallback
|
|
181
|
-
server.middlewares.use(async (req, res, next) => {
|
|
182
|
-
// Skip if this is not a GET request or the request is not for HTML
|
|
183
|
-
if (!req.url || req.method !== 'GET' || !req.headers.accept?.includes('text/html')) {
|
|
184
|
-
return next();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const html = await server.transformIndexHtml(req.url, indexTemplate, req.originalUrl);
|
|
188
|
-
|
|
189
|
-
res.writeHead(200, {
|
|
190
|
-
'content-type': 'text/html',
|
|
191
|
-
'content-length': Buffer.byteLength(html),
|
|
192
|
-
'cache-control': 'no-cache',
|
|
193
|
-
...server.config.server.headers,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return res.end(html);
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
export default plugin;
|
package/src/types.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import type { ConfigEnv } from 'vite';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Base type for template environment variables.
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* A loose record that can hold any key-value pairs later flattened into
|
|
8
|
-
* `FUSION_SPA_*` environment variables by {@link objectToEnv}.
|
|
9
|
-
* Extend this type (or use {@link FusionTemplateEnv}) when you need a
|
|
10
|
-
* strongly-typed environment shape.
|
|
11
|
-
*/
|
|
12
|
-
export type TemplateEnv = Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Describes a single resource that the SPA service worker should intercept.
|
|
16
|
-
*
|
|
17
|
-
* When the service worker sees a fetch request whose URL starts with {@link url},
|
|
18
|
-
* it optionally rewrites the path and attaches an OAuth Bearer token obtained
|
|
19
|
-
* from the MSAL module for the specified {@link scopes}.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* const resource: ResourceConfiguration = {
|
|
24
|
-
* url: '/app-proxy',
|
|
25
|
-
* rewrite: '/@fusion-api/app',
|
|
26
|
-
* scopes: ['api://backend/.default'],
|
|
27
|
-
* };
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export type ResourceConfiguration = {
|
|
31
|
-
/** URL path prefix to match against outgoing fetch requests. */
|
|
32
|
-
url: string;
|
|
33
|
-
/** OAuth scopes used to acquire a Bearer token for matched requests. */
|
|
34
|
-
scopes?: string[];
|
|
35
|
-
/** Replacement path prefix; the matched {@link url} segment is swapped for this value. */
|
|
36
|
-
rewrite?: string;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Strongly-typed environment configuration consumed by the default SPA
|
|
41
|
-
* HTML template and bootstrap script.
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* Values are flattened to `FUSION_SPA_*` environment variables at build time
|
|
45
|
-
* and injected into the HTML template via Vite's
|
|
46
|
-
* {@link https://vite.dev/guide/env-and-mode.html#html-constant-replacement | constant replacement}.
|
|
47
|
-
* They can also be overridden through a `.env` file.
|
|
48
|
-
*
|
|
49
|
-
* @see {@link PluginOptions.generateTemplateEnv} for how to supply these values.
|
|
50
|
-
*/
|
|
51
|
-
export type FusionTemplateEnv = {
|
|
52
|
-
/** HTML `<title>` for the generated page. */
|
|
53
|
-
title: string;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Path to the bootstrap module loaded by the HTML template.
|
|
57
|
-
*
|
|
58
|
-
* @defaultValue `'/@fusion-spa-bootstrap.js'`
|
|
59
|
-
*/
|
|
60
|
-
bootstrap: string;
|
|
61
|
-
|
|
62
|
-
/** Optional telemetry configuration. */
|
|
63
|
-
telemetry?: {
|
|
64
|
-
/**
|
|
65
|
-
* Minimum severity level for console telemetry output.
|
|
66
|
-
*
|
|
67
|
-
* @remarks
|
|
68
|
-
* Maps to `TelemetryLevel` values: Debug (0), Information (1),
|
|
69
|
-
* Warning (2), Error (3), Critical (4).
|
|
70
|
-
*
|
|
71
|
-
* @defaultValue `1` (Information)
|
|
72
|
-
*/
|
|
73
|
-
consoleLevel?: number;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Portal to load and render inside the SPA shell.
|
|
78
|
-
*
|
|
79
|
-
* @remarks
|
|
80
|
-
* The `id` can reference:
|
|
81
|
-
* - A local npm package (e.g. `@equinor/fusion-framework-dev-portal`)
|
|
82
|
-
* - A portal identifier from the Fusion Portal Service
|
|
83
|
-
* - Any custom portal implementation
|
|
84
|
-
*/
|
|
85
|
-
portal: {
|
|
86
|
-
/** Portal identifier used to fetch the portal manifest. */
|
|
87
|
-
id: string;
|
|
88
|
-
/**
|
|
89
|
-
* Version tag for the portal manifest.
|
|
90
|
-
* @defaultValue `'latest'`
|
|
91
|
-
*/
|
|
92
|
-
tag?: string;
|
|
93
|
-
/**
|
|
94
|
-
* When `true`, portal entry point requests are prefixed with `/portal-proxy`
|
|
95
|
-
* so they can be intercepted by a proxy server.
|
|
96
|
-
* @defaultValue `false`
|
|
97
|
-
*/
|
|
98
|
-
proxy?: boolean;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/** Service discovery endpoint and authentication scopes. */
|
|
102
|
-
serviceDiscovery: {
|
|
103
|
-
/** URL of the Fusion service discovery endpoint. */
|
|
104
|
-
url: string;
|
|
105
|
-
/** OAuth scopes required to authenticate service discovery requests. */
|
|
106
|
-
scopes: string[];
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/** Microsoft Authentication Library (MSAL) configuration for Azure AD. */
|
|
110
|
-
msal: {
|
|
111
|
-
/** Azure AD tenant identifier. */
|
|
112
|
-
tenantId: string;
|
|
113
|
-
/** Application (client) identifier registered in Azure AD. */
|
|
114
|
-
clientId: string;
|
|
115
|
-
/** Redirect URI for the authentication callback. */
|
|
116
|
-
redirectUri: string;
|
|
117
|
-
/**
|
|
118
|
-
* When `'true'`, the application automatically prompts for login
|
|
119
|
-
* on initial load.
|
|
120
|
-
*/
|
|
121
|
-
requiresAuth: string;
|
|
122
|
-
/**
|
|
123
|
-
* When `'true'`, authentication is served by an in-process mock client
|
|
124
|
-
* instead of Entra ID — no credentials, redirects, or network calls.
|
|
125
|
-
* Intended for CI/Playwright runs against the dev server.
|
|
126
|
-
*/
|
|
127
|
-
mock?: string | boolean;
|
|
128
|
-
/**
|
|
129
|
-
* A mock JWT (e.g. from `createMockToken`) whose payload claims (`name`,
|
|
130
|
-
* `preferred_username`, `oid`, `tid`, `scp`) become the signed-in mock
|
|
131
|
-
* user. Only read when {@link mock} is set; ignored otherwise.
|
|
132
|
-
*/
|
|
133
|
-
mockToken?: string;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/** Service worker resource interception configuration. */
|
|
137
|
-
serviceWorker: {
|
|
138
|
-
/**
|
|
139
|
-
* Array of resource configurations the service worker will manage.
|
|
140
|
-
* @see {@link ResourceConfiguration}
|
|
141
|
-
*/
|
|
142
|
-
resources: ResourceConfiguration[];
|
|
143
|
-
};
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Factory function that produces a partial template environment from the
|
|
148
|
-
* current Vite {@link ConfigEnv} (mode, command, etc.).
|
|
149
|
-
*
|
|
150
|
-
* @remarks
|
|
151
|
-
* Returned values are merged with defaults and flattened into
|
|
152
|
-
* `FUSION_SPA_*` environment variables.
|
|
153
|
-
*
|
|
154
|
-
* @template TEnv - Shape of the environment configuration. Defaults to {@link TemplateEnv}.
|
|
155
|
-
* @param configEnv - Vite configuration environment containing `mode` and `command`.
|
|
156
|
-
* @returns A partial environment object, or `undefined` to use defaults only.
|
|
157
|
-
*/
|
|
158
|
-
export type TemplateEnvFn<TEnv extends TemplateEnv> = (
|
|
159
|
-
configEnv: ConfigEnv,
|
|
160
|
-
) => Partial<TEnv> | undefined;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type ConfigEnv, loadEnv, type UserConfig } from 'vite';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Loads environment variables from `.env` files for the Fusion SPA plugin.
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* Delegates to Vite's {@link https://vite.dev/guide/env-and-mode.html#env-files | loadEnv}
|
|
9
|
-
* using the resolved project root and env directory. Only variables whose
|
|
10
|
-
* name starts with {@link namespace} are returned.
|
|
11
|
-
*
|
|
12
|
-
* Values loaded here override any matching keys produced by
|
|
13
|
-
* {@link PluginOptions.generateTemplateEnv}.
|
|
14
|
-
*
|
|
15
|
-
* @param config - Vite user configuration (`root`, `envDir`).
|
|
16
|
-
* @param env - Vite configuration environment containing the current `mode`.
|
|
17
|
-
* @param namespace - Variable name prefix to filter on.
|
|
18
|
-
* @returns A flat record of matching environment variable names to their string values.
|
|
19
|
-
*
|
|
20
|
-
* @defaultValue namespace — `'FUSION_SPA_'`
|
|
21
|
-
*/
|
|
22
|
-
export function loadEnvironment(
|
|
23
|
-
config: UserConfig,
|
|
24
|
-
env: ConfigEnv,
|
|
25
|
-
namespace = 'FUSION_SPA_',
|
|
26
|
-
): Record<string, string> {
|
|
27
|
-
// resolve the root directory
|
|
28
|
-
const resolvedRoot = resolve(config.root || process.cwd());
|
|
29
|
-
// resolve the environment directory
|
|
30
|
-
const envDir = config.envDir ? resolve(resolvedRoot, config.envDir) : resolvedRoot;
|
|
31
|
-
// load environment variables from the specified directory
|
|
32
|
-
return loadEnv(env.mode, envDir, namespace);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default loadEnvironment;
|