@sap-ux/preview-middleware 1.0.41 → 1.0.44

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.
@@ -20,57 +20,55 @@ sap.ui.define(["sap/base/Log", "open/ux/preview/client/thirdparty/@sap-ux-privat
20
20
  }
21
21
 
22
22
  /**
23
- * Builds a lookup map from module name patterns to change metadata
24
- * for addXML and codeExt changes.
23
+ * Builds the set of module name patterns to watch for in error messages,
24
+ * derived from addXML and codeExt changes.
25
25
  *
26
26
  * @param changes record of change objects keyed by flex key
27
- * @returns map from module name substring to orphaned change entry
27
+ * @returns set of module name substrings identifying orphaned-change error messages
28
28
  */
29
- function buildModuleNameMap(changes) {
30
- const map = new Map();
29
+ function buildModuleNameSet(changes) {
30
+ const moduleNames = new Set();
31
31
  for (const change of Object.values(changes)) {
32
32
  if (!isFragmentOrCodeExtChange(change)) {
33
33
  continue;
34
34
  }
35
35
  const prefix = change.reference.replaceAll('.', '/');
36
36
  const path = change.changeType === CHANGE_TYPE.addXML ? change.content?.fragmentPath ?? '' : change.content?.codeRef ?? '';
37
- const changeFileName = `${change.fileName}.${change.fileType ?? 'change'}`;
38
- const key = change.moduleName ?? `${prefix}/changes/${path}`;
39
- map.set(key, {
40
- changeFileName,
41
- filePath: path,
42
- changeType: change.changeType
43
- });
37
+ moduleNames.add(change.moduleName || `${prefix}/changes/${path}`); // || not ?? — empty string is falsy, fall back to computed path
44
38
  }
45
- return map;
39
+ return moduleNames;
46
40
  }
47
41
 
42
+ /**
43
+ * UI5's `sap/base/Log` prefixes every message with a timestamp
44
+ * (`YYYY-MM-DD HH:MM:SS.mmmmmm `). Strip it so the InfoCenter shows just the error text.
45
+ */
46
+ const UI5_LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+/;
47
+
48
48
  /**
49
49
  * Creates an error handler that matches error messages against known module names
50
- * and sends InfoCenter errors for orphaned change files.
50
+ * and forwards the original browser error to the InfoCenter.
51
51
  *
52
- * @param moduleNameMap map from module name substring to orphaned change entry
52
+ * @param moduleNames set of module name substrings to watch for
53
+ * @param onAllMatched callback invoked once every watched module name has been matched
53
54
  * @returns error handler function
54
55
  */
55
- function createErrorHandler(moduleNameMap, restoreConsole) {
56
+ function createErrorHandler(moduleNames, onAllMatched) {
56
57
  return message => {
57
- for (const [moduleName, entry] of moduleNameMap) {
58
+ for (const moduleName of moduleNames) {
58
59
  if (message.includes(moduleName)) {
59
60
  sendInfoCenterMessage({
60
61
  title: {
61
- key: 'ADP_ORPHANED_CHANGE_ERROR_TITLE'
62
- },
63
- description: {
64
- key: 'ADP_ORPHANED_FILE_DESCRIPTION',
65
- params: [entry.filePath, entry.changeFileName]
62
+ key: 'ADP_CHANGE_ERROR_TITLE'
66
63
  },
64
+ description: message.replace(UI5_LOG_TIMESTAMP, ''),
67
65
  type: MessageBarType.error
68
66
  }).catch(error => {
69
67
  log.error('Failed to send orphaned change InfoCenter message', error);
70
68
  });
71
- moduleNameMap.delete(moduleName);
72
- if (moduleNameMap.size === 0) {
73
- restoreConsole();
69
+ moduleNames.delete(moduleName);
70
+ if (moduleNames.size === 0) {
71
+ onAllMatched();
74
72
  }
75
73
  break;
76
74
  }
@@ -81,43 +79,91 @@ sap.ui.define(["sap/base/Log", "open/ux/preview/client/thirdparty/@sap-ux-privat
81
79
  /**
82
80
  * Initializes orphaned change file detection.
83
81
  *
84
- * Fetches loaded flex changes, builds a lookup map of module names for addXML and codeExt changes,
85
- * and wraps console.error to intercept UI5 flex change application errors. When UI5 fails to load
86
- * a fragment or controller extension referenced by a change file, the error is intercepted and an
87
- * actionable message is shown in the InfoCenter advising the user to delete the orphaned change file.
82
+ * Wraps `console.error` synchronously so early errors (emitted during app bootstrap, before
83
+ * the changes list has been fetched) are captured in a buffer. Once the changes are loaded,
84
+ * the buffered messages are replayed against the set of module names derived from addXML and
85
+ * codeExt changes. Subsequent errors are matched live. When UI5 fails to load a fragment or
86
+ * controller extension referenced by a change file, the original error is forwarded to the
87
+ * InfoCenter so the user sees the exact browser message.
88
+ *
89
+ * @returns a cancel function — call it to stop detection and immediately restore console.error.
88
90
  */
89
- async function initOrphanedChangeDetection() {
90
- const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
91
- const response = await fetch(`${baseUrl}/preview/api/changes`, {
92
- method: 'GET',
93
- headers: {
94
- 'content-type': 'application/json'
95
- }
96
- });
97
- if (!response.ok) {
98
- log.error(`Failed to fetch changes for orphaned change detection: ${response.status}`);
99
- return;
100
- }
101
- const changes = await response.json();
102
- const moduleNameMap = buildModuleNameMap(changes);
103
- if (moduleNameMap.size === 0) {
104
- return;
105
- }
91
+ function initOrphanedChangeDetection() {
106
92
  const consoleRef = globalThis.console;
107
93
  const originalConsoleError = consoleRef.error;
94
+ const BUFFER_LIMIT = 200;
95
+ const buffer = [];
96
+ let moduleNames;
97
+ let handler;
98
+ let cancelled = false;
108
99
  const restore = () => {
109
100
  consoleRef.error = originalConsoleError;
110
101
  };
111
- const handler = createErrorHandler(moduleNameMap, restore);
112
- const safetyTimeout = setTimeout(restore, 60_000);
102
+ const cancel = () => {
103
+ cancelled = true;
104
+ buffer.length = 0;
105
+ restore();
106
+ clearTimeout(safetyTimeout);
107
+ };
108
+ const safetyTimeout = setTimeout(cancel, 60_000);
113
109
  consoleRef.error = (...args) => {
114
110
  originalConsoleError.apply(consoleRef, args);
111
+ if (cancelled) {
112
+ return;
113
+ }
115
114
  const message = args.filter(arg => typeof arg === 'string').join('');
116
- handler(message);
117
- if (moduleNameMap.size === 0) {
118
- clearTimeout(safetyTimeout);
115
+ if (handler) {
116
+ handler(message);
117
+ if (moduleNames?.size === 0) {
118
+ clearTimeout(safetyTimeout);
119
+ }
120
+ } else if (buffer.length < BUFFER_LIMIT) {
121
+ buffer.push(message);
119
122
  }
120
123
  };
124
+ void loadModuleNamesAndReplay();
125
+ async function loadModuleNamesAndReplay() {
126
+ try {
127
+ const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
128
+ const response = await fetch(`${baseUrl}/preview/api/changes`, {
129
+ method: 'GET',
130
+ headers: {
131
+ 'content-type': 'application/json'
132
+ }
133
+ });
134
+ if (cancelled) {
135
+ return;
136
+ }
137
+ if (!response.ok) {
138
+ log.error(`Failed to fetch changes for orphaned change detection: ${response.status}`);
139
+ cancel();
140
+ return;
141
+ }
142
+ const changes = await response.json();
143
+ if (cancelled) {
144
+ return;
145
+ }
146
+ moduleNames = buildModuleNameSet(changes);
147
+ if (moduleNames.size === 0) {
148
+ cancel();
149
+ return;
150
+ }
151
+ handler = createErrorHandler(moduleNames, cancel);
152
+
153
+ // Replay any errors emitted before the changes list was loaded.
154
+ for (const message of buffer) {
155
+ handler(message);
156
+ if (moduleNames.size === 0) {
157
+ break;
158
+ }
159
+ }
160
+ buffer.length = 0;
161
+ } catch (error) {
162
+ log.error('Failed to run orphaned change detection', error);
163
+ cancel();
164
+ }
165
+ }
166
+ return cancel;
121
167
  }
122
168
  var __exports = {
123
169
  __esModule: true
@@ -8,8 +8,6 @@ const CHANGE_TYPE = {
8
8
  codeExt: 'codeExt'
9
9
  };
10
10
 
11
- type FlexChangeType = (typeof CHANGE_TYPE)[keyof typeof CHANGE_TYPE];
12
-
13
11
  interface ChangeContent {
14
12
  fragmentPath?: string;
15
13
  codeRef?: string;
@@ -24,14 +22,8 @@ interface Change {
24
22
  content?: ChangeContent;
25
23
  }
26
24
 
27
- interface OrphanedChangeEntry {
28
- changeFileName: string;
29
- filePath: string;
30
- changeType: FlexChangeType;
31
- }
32
-
33
25
  interface RelevantChange extends Change {
34
- changeType: FlexChangeType;
26
+ changeType: (typeof CHANGE_TYPE)[keyof typeof CHANGE_TYPE];
35
27
  reference: string;
36
28
  }
37
29
 
@@ -50,14 +42,14 @@ function isFragmentOrCodeExtChange(change: Change): change is RelevantChange {
50
42
  }
51
43
 
52
44
  /**
53
- * Builds a lookup map from module name patterns to change metadata
54
- * for addXML and codeExt changes.
45
+ * Builds the set of module name patterns to watch for in error messages,
46
+ * derived from addXML and codeExt changes.
55
47
  *
56
48
  * @param changes record of change objects keyed by flex key
57
- * @returns map from module name substring to orphaned change entry
49
+ * @returns set of module name substrings identifying orphaned-change error messages
58
50
  */
59
- function buildModuleNameMap(changes: Record<string, Change>): Map<string, OrphanedChangeEntry> {
60
- const map = new Map<string, OrphanedChangeEntry>();
51
+ function buildModuleNameSet(changes: Record<string, Change>): Set<string> {
52
+ const moduleNames = new Set<string>();
61
53
 
62
54
  for (const change of Object.values(changes)) {
63
55
  if (!isFragmentOrCodeExtChange(change)) {
@@ -65,43 +57,47 @@ function buildModuleNameMap(changes: Record<string, Change>): Map<string, Orphan
65
57
  }
66
58
 
67
59
  const prefix = change.reference.replaceAll('.', '/');
68
- const path = change.changeType === CHANGE_TYPE.addXML ? change.content?.fragmentPath ?? '' : change.content?.codeRef ?? '';
69
- const changeFileName = `${change.fileName}.${change.fileType ?? 'change'}`;
70
- const key = change.moduleName ?? `${prefix}/changes/${path}`;
71
-
72
- map.set(key, { changeFileName, filePath: path, changeType: change.changeType });
60
+ const path =
61
+ change.changeType === CHANGE_TYPE.addXML
62
+ ? change.content?.fragmentPath ?? ''
63
+ : change.content?.codeRef ?? '';
64
+ moduleNames.add(change.moduleName || `${prefix}/changes/${path}`); // || not ?? — empty string is falsy, fall back to computed path
73
65
  }
74
66
 
75
- return map;
67
+ return moduleNames;
76
68
  }
77
69
 
70
+ /**
71
+ * UI5's `sap/base/Log` prefixes every message with a timestamp
72
+ * (`YYYY-MM-DD HH:MM:SS.mmmmmm `). Strip it so the InfoCenter shows just the error text.
73
+ */
74
+ const UI5_LOG_TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+/;
75
+
78
76
  /**
79
77
  * Creates an error handler that matches error messages against known module names
80
- * and sends InfoCenter errors for orphaned change files.
78
+ * and forwards the original browser error to the InfoCenter.
81
79
  *
82
- * @param moduleNameMap map from module name substring to orphaned change entry
80
+ * @param moduleNames set of module name substrings to watch for
81
+ * @param onAllMatched callback invoked once every watched module name has been matched
83
82
  * @returns error handler function
84
83
  */
85
84
  function createErrorHandler(
86
- moduleNameMap: Map<string, OrphanedChangeEntry>,
87
- restoreConsole: () => void
85
+ moduleNames: Set<string>,
86
+ onAllMatched: () => void
88
87
  ): (message: string) => void {
89
88
  return (message: string) => {
90
- for (const [moduleName, entry] of moduleNameMap) {
89
+ for (const moduleName of moduleNames) {
91
90
  if (message.includes(moduleName)) {
92
91
  sendInfoCenterMessage({
93
- title: { key: 'ADP_ORPHANED_CHANGE_ERROR_TITLE' },
94
- description: {
95
- key: 'ADP_ORPHANED_FILE_DESCRIPTION',
96
- params: [entry.filePath, entry.changeFileName]
97
- },
92
+ title: { key: 'ADP_CHANGE_ERROR_TITLE' },
93
+ description: message.replace(UI5_LOG_TIMESTAMP, ''),
98
94
  type: MessageBarType.error
99
95
  }).catch((error) => {
100
96
  log.error('Failed to send orphaned change InfoCenter message', error);
101
97
  });
102
- moduleNameMap.delete(moduleName);
103
- if (moduleNameMap.size === 0) {
104
- restoreConsole();
98
+ moduleNames.delete(moduleName);
99
+ if (moduleNames.size === 0) {
100
+ onAllMatched();
105
101
  }
106
102
  break;
107
103
  }
@@ -112,47 +108,102 @@ function createErrorHandler(
112
108
  /**
113
109
  * Initializes orphaned change file detection.
114
110
  *
115
- * Fetches loaded flex changes, builds a lookup map of module names for addXML and codeExt changes,
116
- * and wraps console.error to intercept UI5 flex change application errors. When UI5 fails to load
117
- * a fragment or controller extension referenced by a change file, the error is intercepted and an
118
- * actionable message is shown in the InfoCenter advising the user to delete the orphaned change file.
111
+ * Wraps `console.error` synchronously so early errors (emitted during app bootstrap, before
112
+ * the changes list has been fetched) are captured in a buffer. Once the changes are loaded,
113
+ * the buffered messages are replayed against the set of module names derived from addXML and
114
+ * codeExt changes. Subsequent errors are matched live. When UI5 fails to load a fragment or
115
+ * controller extension referenced by a change file, the original error is forwarded to the
116
+ * InfoCenter so the user sees the exact browser message.
117
+ *
118
+ * @returns a cancel function — call it to stop detection and immediately restore console.error.
119
119
  */
120
- export async function initOrphanedChangeDetection(): Promise<void> {
121
- const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
122
- const response = await fetch(`${baseUrl}/preview/api/changes`, {
123
- method: 'GET',
124
- headers: { 'content-type': 'application/json' }
125
- });
126
-
127
- if (!response.ok) {
128
- log.error(`Failed to fetch changes for orphaned change detection: ${response.status}`);
129
- return;
130
- }
131
-
132
- const changes = (await response.json()) as Record<string, Change>;
133
- const moduleNameMap = buildModuleNameMap(changes);
134
-
135
- if (moduleNameMap.size === 0) {
136
- return;
137
- }
138
-
120
+ export function initOrphanedChangeDetection(): () => void {
139
121
  const consoleRef = globalThis.console;
140
122
  const originalConsoleError = consoleRef.error;
141
123
 
124
+ const BUFFER_LIMIT = 200;
125
+ const buffer: string[] = [];
126
+ let moduleNames: Set<string> | undefined;
127
+ let handler: ((message: string) => void) | undefined;
128
+ let cancelled = false;
129
+
142
130
  const restore = (): void => {
143
131
  consoleRef.error = originalConsoleError;
144
132
  };
145
133
 
146
- const handler = createErrorHandler(moduleNameMap, restore);
134
+ const cancel = (): void => {
135
+ cancelled = true;
136
+ buffer.length = 0;
137
+ restore();
138
+ clearTimeout(safetyTimeout);
139
+ };
147
140
 
148
- const safetyTimeout = setTimeout(restore, 60_000);
141
+ const safetyTimeout = setTimeout(cancel, 60_000);
149
142
 
150
143
  consoleRef.error = (...args: unknown[]) => {
151
144
  originalConsoleError.apply(consoleRef, args);
145
+ if (cancelled) {
146
+ return;
147
+ }
152
148
  const message = args.filter((arg): arg is string => typeof arg === 'string').join('');
153
- handler(message);
154
- if (moduleNameMap.size === 0) {
155
- clearTimeout(safetyTimeout);
149
+ if (handler) {
150
+ handler(message);
151
+ if (moduleNames?.size === 0) {
152
+ clearTimeout(safetyTimeout);
153
+ }
154
+ } else if (buffer.length < BUFFER_LIMIT) {
155
+ buffer.push(message);
156
156
  }
157
157
  };
158
+
159
+ void loadModuleNamesAndReplay();
160
+
161
+ async function loadModuleNamesAndReplay(): Promise<void> {
162
+ try {
163
+ const baseUrl = document.getElementById('sap-ui-bootstrap')?.dataset.openUxPreviewBaseUrl ?? '';
164
+ const response = await fetch(`${baseUrl}/preview/api/changes`, {
165
+ method: 'GET',
166
+ headers: { 'content-type': 'application/json' }
167
+ });
168
+
169
+ if (cancelled) {
170
+ return;
171
+ }
172
+
173
+ if (!response.ok) {
174
+ log.error(`Failed to fetch changes for orphaned change detection: ${response.status}`);
175
+ cancel();
176
+ return;
177
+ }
178
+
179
+ const changes = (await response.json()) as Record<string, Change>;
180
+
181
+ if (cancelled) {
182
+ return;
183
+ }
184
+
185
+ moduleNames = buildModuleNameSet(changes);
186
+
187
+ if (moduleNames.size === 0) {
188
+ cancel();
189
+ return;
190
+ }
191
+
192
+ handler = createErrorHandler(moduleNames, cancel);
193
+
194
+ // Replay any errors emitted before the changes list was loaded.
195
+ for (const message of buffer) {
196
+ handler(message);
197
+ if (moduleNames.size === 0) {
198
+ break;
199
+ }
200
+ }
201
+ buffer.length = 0;
202
+ } catch (error) {
203
+ log.error('Failed to run orphaned change detection', error as Error);
204
+ cancel();
205
+ }
206
+ }
207
+
208
+ return cancel;
158
209
  }
@@ -40,6 +40,7 @@ sap.ui.define(["sap/base/Log", "open/ux/preview/client/thirdparty/@sap-ux-privat
40
40
  const CommunicationService = ___cpe_communication_servicejs["CommunicationService"];
41
41
  const initOrphanedChangeDetection = ___change_file_validatorjs["initOrphanedChangeDetection"];
42
42
  var __exports = async function (rta) {
43
+ const cancelOrphanedChangeDetection = initOrphanedChangeDetection();
43
44
  const flexSettings = rta.getFlexSettings();
44
45
  if (flexSettings.telemetry === true) {
45
46
  enableTelemetry();
@@ -103,11 +104,9 @@ sap.ui.define(["sap/base/Log", "open/ux/preview/client/thirdparty/@sap-ux-privat
103
104
  type: MessageBarType.error
104
105
  });
105
106
  CommunicationService.sendAction(toggleAppPreviewVisibility(false));
107
+ cancelOrphanedChangeDetection();
106
108
  return;
107
109
  }
108
- initOrphanedChangeDetection().catch(error => {
109
- log.error('Failed to run orphaned change detection', error);
110
- });
111
110
  log.debug('ADP init executed.');
112
111
  };
113
112
  return __exports;
@@ -26,6 +26,8 @@ import { CommunicationService } from '../cpe/communication-service.js';
26
26
  import { initOrphanedChangeDetection } from './change-file-validator.js';
27
27
 
28
28
  export default async function (rta: RuntimeAuthoring) {
29
+ const cancelOrphanedChangeDetection = initOrphanedChangeDetection();
30
+
29
31
  const flexSettings = rta.getFlexSettings();
30
32
  if (flexSettings.telemetry === true) {
31
33
  enableTelemetry();
@@ -86,12 +88,9 @@ export default async function (rta: RuntimeAuthoring) {
86
88
  type: MessageBarType.error
87
89
  });
88
90
  CommunicationService.sendAction(toggleAppPreviewVisibility(false));
91
+ cancelOrphanedChangeDetection();
89
92
  return;
90
93
  }
91
94
 
92
- initOrphanedChangeDetection().catch((error) => {
93
- log.error('Failed to run orphaned change detection', error);
94
- });
95
-
96
95
  log.debug('ADP init executed.');
97
96
  }
@@ -67,8 +67,7 @@ ADP_CREATE_CONTROLLER_EXTENSION_TITLE = Create Controller Extension
67
67
  ADP_CREATE_CONTROLLER_EXTENSION_DESCRIPTION = Controller extension with name ''{0}'' was created.
68
68
  ADP_ODATA_HEALTH_CHECK_TITLE = OData Service Health Check
69
69
  ADP_ODATA_SERVICE_DOWN_DESCRIPTION = The OData service with the {0} endpoint is down. Error: {1}.
70
- ADP_ORPHANED_CHANGE_ERROR_TITLE = Missing File Detected
71
- ADP_ORPHANED_FILE_DESCRIPTION = The "{0}" file referenced by the "{1}" change was not found. To resolve this error, delete the change file.
70
+ ADP_CHANGE_ERROR_TITLE = Change Error
72
71
  ADP_ADD_ACTION_DIALOG_ACTION_ID_LABEL = Action ID
73
72
  ADP_ADD_ACTION_DIALOG_BUTTON_TEXT_LABEL = Button Text
74
73
  ADP_ADD_ACTION_DIALOG_HANDLER_METHOD_LABEL = Handler Method
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "bugs": {
11
11
  "url": "https://github.com/SAP/open-ux-tools/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apreview-middleware"
12
12
  },
13
- "version": "1.0.41",
13
+ "version": "1.0.44",
14
14
  "license": "Apache-2.0",
15
15
  "author": "@SAP/ux-tools-team",
16
16
  "main": "dist/index.js",
@@ -28,12 +28,12 @@
28
28
  "mem-fs-editor": "9.4.0",
29
29
  "qrcode": "1.5.4",
30
30
  "@sap/bas-sdk": "3.13.9",
31
- "@sap-ux/adp-tooling": "1.0.32",
31
+ "@sap-ux/adp-tooling": "1.0.34",
32
32
  "@sap-ux/btp-utils": "2.0.5",
33
33
  "@sap-ux/control-property-editor-sources": "npm:@sap-ux/control-property-editor@1.0.7",
34
34
  "@sap-ux/feature-toggle": "1.0.5",
35
35
  "@sap-ux/logger": "1.0.3",
36
- "@sap-ux/project-access": "2.1.6",
36
+ "@sap-ux/project-access": "2.1.7",
37
37
  "@sap-ux/system-access": "1.0.7",
38
38
  "@sap-ux/i18n": "1.0.2"
39
39
  },
@@ -54,7 +54,7 @@
54
54
  "nock": "14.0.15",
55
55
  "npm-run-all2": "8.0.4",
56
56
  "supertest": "7.2.2",
57
- "@private/preview-middleware-client": "npm:@sap-ux-private/preview-middleware-client@1.0.41",
57
+ "@private/preview-middleware-client": "npm:@sap-ux-private/preview-middleware-client@1.0.44",
58
58
  "@sap-ux-private/playwright": "1.0.5",
59
59
  "@sap-ux/axios-extension": "2.0.7",
60
60
  "@sap-ux/store": "2.0.4",