@openmrs/esm-dynamic-loading 9.0.3-pre.4533 → 9.0.3-pre.4550

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.
@@ -0,0 +1,285 @@
1
+ /** @module @category Route Map */
2
+ import { isOpenmrsAppRoutes, isOpenmrsRoutes, type OpenmrsAppRoutes, type OpenmrsRoutes } from '@openmrs/esm-globals';
3
+
4
+ const OVERRIDE_PREFIX = 'openmrs-routes:';
5
+ const CHANGE_EVENT = 'openmrs-routes:change';
6
+
7
+ // Set by setupRouteMapOverrides(); controls whether override functionality is active.
8
+ let devMode = false;
9
+
10
+ // Snapshot of overrides at setup time (mirrors the import-map-overrides pattern:
11
+ // getCurrentRouteMap returns the overrides as they were when the page loaded).
12
+ let initialOverrideSnapshot: OpenmrsRoutes | null = null;
13
+
14
+ /**
15
+ * Reads all `<script type="openmrs-routes">` tags from the DOM and merges
16
+ * them into a single {@link OpenmrsRoutes} object. Tags with a `src` attribute
17
+ * are fetched; inline tags have their `textContent` parsed as JSON.
18
+ */
19
+ async function readBaseMap(): Promise<OpenmrsRoutes> {
20
+ const scripts = document.querySelectorAll<HTMLScriptElement>("script[type='openmrs-routes']");
21
+ const maps: OpenmrsRoutes[] = [];
22
+
23
+ for (let i = 0; i < scripts.length; i++) {
24
+ const script = scripts[i];
25
+ try {
26
+ let parsed: unknown;
27
+ if (script.src) {
28
+ const response = await fetch(script.src);
29
+ parsed = await response.json();
30
+ } else if (script.textContent) {
31
+ parsed = JSON.parse(script.textContent);
32
+ }
33
+
34
+ if (parsed && isOpenmrsRoutes(parsed)) {
35
+ maps.push(parsed);
36
+ }
37
+ } catch (e) {
38
+ console.warn(`[route-maps] Failed to parse routes from script tag at index ${i}`, e);
39
+ }
40
+ }
41
+
42
+ return mergeRouteMaps(maps);
43
+ }
44
+
45
+ function mergeRouteMaps(maps: OpenmrsRoutes[]): OpenmrsRoutes {
46
+ const merged: OpenmrsRoutes = {};
47
+ for (const map of maps) {
48
+ if (map && typeof map === 'object') {
49
+ Object.assign(merged, map);
50
+ }
51
+ }
52
+ return merged;
53
+ }
54
+
55
+ /**
56
+ * Reads all `openmrs-routes:*` entries from localStorage as an {@link OpenmrsRoutes}.
57
+ * This is async because URL-valued overrides need to be fetched.
58
+ */
59
+ async function readOverrideMap(): Promise<OpenmrsRoutes> {
60
+ const result: OpenmrsRoutes = {};
61
+
62
+ try {
63
+ const entries: Array<{ moduleName: string; raw: string }> = [];
64
+ for (let i = 0; i < localStorage.length; i++) {
65
+ const key = localStorage.key(i);
66
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
67
+ const raw = localStorage.getItem(key);
68
+ if (raw) {
69
+ entries.push({ moduleName: key.slice(OVERRIDE_PREFIX.length), raw });
70
+ }
71
+ }
72
+ }
73
+
74
+ const settled = await Promise.allSettled(
75
+ entries.map(async ({ moduleName, raw }) => {
76
+ const parsed = JSON.parse(raw);
77
+
78
+ if (isOpenmrsAppRoutes(parsed)) {
79
+ return { moduleName, routes: parsed };
80
+ }
81
+
82
+ if (typeof parsed === 'string' && parsed.startsWith('http')) {
83
+ const response = await fetch(parsed);
84
+ const fetched: unknown = await response.json();
85
+ if (isOpenmrsAppRoutes(fetched)) {
86
+ return { moduleName, routes: fetched };
87
+ }
88
+ throw new Error(`${parsed} did not resolve to a valid OpenmrsAppRoutes object`);
89
+ }
90
+
91
+ throw new Error(`Override for ${moduleName} is neither a valid routes object nor a URL`);
92
+ }),
93
+ );
94
+
95
+ for (const entry of settled) {
96
+ if (entry.status === 'fulfilled') {
97
+ result[entry.value.moduleName] = entry.value.routes;
98
+ } else {
99
+ console.warn('[route-maps] Failed to load route override', entry.reason);
100
+ }
101
+ }
102
+ } catch (e) {
103
+ console.warn('[route-maps] Failed to read route overrides from localStorage', e);
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Returns the route map for the current page. In dev mode, this merges
111
+ * the base map with the override snapshot captured at page load.
112
+ */
113
+ export async function getCurrentRouteMap(): Promise<OpenmrsRoutes> {
114
+ const base = await readBaseMap();
115
+ if (!devMode) {
116
+ return base;
117
+ }
118
+ const overrides = initialOverrideSnapshot ?? (await readOverrideMap());
119
+ return mergeRouteMaps([base, overrides]);
120
+ }
121
+
122
+ /**
123
+ * Returns the base route map from the DOM without any overrides applied.
124
+ */
125
+ export async function getRouteMapDefaultMap(): Promise<OpenmrsRoutes> {
126
+ return readBaseMap();
127
+ }
128
+
129
+ /**
130
+ * Returns what the route map will look like on the next page load, including
131
+ * any overrides that have been added/removed since the page loaded.
132
+ * In production, this is the same as the base map.
133
+ */
134
+ export async function getRouteMapNextPageMap(): Promise<OpenmrsRoutes> {
135
+ if (!devMode) {
136
+ return readBaseMap();
137
+ }
138
+ const base = await readBaseMap();
139
+ return mergeRouteMaps([base, await readOverrideMap()]);
140
+ }
141
+
142
+ /**
143
+ * Returns the current raw override entries from localStorage.
144
+ * In production, returns an empty object.
145
+ */
146
+ export function getRouteMapOverrideMap(): Record<string, string> {
147
+ if (!devMode) {
148
+ return {};
149
+ }
150
+
151
+ const result: Record<string, string> = {};
152
+ try {
153
+ for (let i = 0; i < localStorage.length; i++) {
154
+ const key = localStorage.key(i);
155
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
156
+ const value = localStorage.getItem(key);
157
+ if (value) {
158
+ result[key.slice(OVERRIDE_PREFIX.length)] = value;
159
+ }
160
+ }
161
+ }
162
+ } catch (e) {
163
+ console.warn('[route-maps] Failed to read route overrides from localStorage', e);
164
+ }
165
+ return result;
166
+ }
167
+
168
+ /**
169
+ * Adds a route override. In production, this is a no-op.
170
+ * The app must be reloaded for overrides to take effect.
171
+ *
172
+ * @param moduleName The name of the module the routes are for
173
+ * @param routes Either an {@link OpenmrsAppRoutes} object, a string that represents a JSON
174
+ * version of an {@link OpenmrsAppRoutes} object, or a string or URL that resolves to a
175
+ * JSON document that represents an {@link OpenmrsAppRoutes} object
176
+ */
177
+ export function addRouteMapOverride(moduleName: string, routes: OpenmrsAppRoutes | string | URL) {
178
+ if (!devMode) {
179
+ console.warn('[Security] Route overrides are disabled outside development mode.');
180
+ return;
181
+ }
182
+
183
+ try {
184
+ if (typeof routes === 'string') {
185
+ if (routes.startsWith('http')) {
186
+ localStorage.setItem(OVERRIDE_PREFIX + moduleName, JSON.stringify(routes));
187
+ } else {
188
+ const maybeRoutes = JSON.parse(routes);
189
+ if (isOpenmrsAppRoutes(maybeRoutes)) {
190
+ localStorage.setItem(OVERRIDE_PREFIX + moduleName, JSON.stringify(maybeRoutes));
191
+ } else {
192
+ console.error(`The supplied routes for ${moduleName} is not a valid OpenmrsAppRoutes object`, routes);
193
+ return;
194
+ }
195
+ }
196
+ } else if (routes instanceof URL) {
197
+ localStorage.setItem(OVERRIDE_PREFIX + moduleName, JSON.stringify(routes.toString()));
198
+ } else if (isOpenmrsAppRoutes(routes)) {
199
+ localStorage.setItem(OVERRIDE_PREFIX + moduleName, JSON.stringify(routes));
200
+ } else {
201
+ console.error(
202
+ `Override for ${moduleName} is not in a valid format. Expected either a Javascript Object, a JSON string of a Javascript object, or a URL`,
203
+ routes,
204
+ );
205
+ return;
206
+ }
207
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
208
+ } catch (e) {
209
+ console.warn('[route-maps] Failed to write route override to localStorage', e);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Removes a route override. In production, this is a no-op.
215
+ * The app must be reloaded for the removal to take effect.
216
+ *
217
+ * @param moduleName The module to remove the overrides for
218
+ */
219
+ export function removeRouteMapOverride(moduleName: string) {
220
+ if (!devMode) {
221
+ console.warn('[Security] Route overrides are disabled outside development mode.');
222
+ return;
223
+ }
224
+
225
+ try {
226
+ localStorage.removeItem(OVERRIDE_PREFIX + moduleName);
227
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
228
+ } catch (e) {
229
+ console.warn('[route-maps] Failed to remove route override from localStorage', e);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Clears all route overrides. In production, this is a no-op.
235
+ * The app must be reloaded for the removal to take effect.
236
+ */
237
+ export function resetRouteMapOverrides() {
238
+ if (!devMode) {
239
+ console.warn('[Security] Route overrides are disabled outside development mode.');
240
+ return;
241
+ }
242
+
243
+ try {
244
+ const keysToRemove: string[] = [];
245
+ for (let i = 0; i < localStorage.length; i++) {
246
+ const key = localStorage.key(i);
247
+ if (key?.startsWith(OVERRIDE_PREFIX)) {
248
+ keysToRemove.push(key);
249
+ }
250
+ }
251
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
252
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT));
253
+ } catch (e) {
254
+ console.warn('[route-maps] Failed to clear route overrides from localStorage', e);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Initializes the route map override system with mode-aware behavior.
260
+ *
261
+ * In production (`window.spaEnv !== 'development'`), mutation functions are
262
+ * no-ops and localStorage overrides are never consulted. In development, the
263
+ * full override workflow is available.
264
+ *
265
+ * Must be called before any code that depends on the route map functions.
266
+ */
267
+ let setupPromise: Promise<void> | null = null;
268
+
269
+ export function setupRouteMapOverrides(): Promise<void> {
270
+ if (setupPromise) {
271
+ return setupPromise;
272
+ }
273
+
274
+ devMode = window.spaEnv === 'development';
275
+
276
+ if (devMode) {
277
+ setupPromise = readOverrideMap().then((snapshot) => {
278
+ initialOverrideSnapshot = snapshot;
279
+ });
280
+ } else {
281
+ setupPromise = Promise.resolve();
282
+ }
283
+
284
+ return setupPromise;
285
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ mockReset: true,
7
+ },
8
+ });