Skip to content

Commit 7be129c

Browse files
fix: feedback
1 parent 45ead4f commit 7be129c

4 files changed

Lines changed: 256 additions & 129 deletions

File tree

.changeset/funny-socks-rescue.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
'@sap-ux/project-access': patch
2+
'@sap-ux/project-access': minor
33
'@sap-ux/fe-fpm-writer': patch
44
---
55

packages/project-access/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,6 @@ export {
7171
} from './project';
7272
export { execNpmCommand } from './command/npm-command';
7373
export * from './types';
74-
export * from './library';
74+
export { checkDependencies, getReuseLibs, validateId } from './library';
7575
export { findRecursiveHierarchyKey, getTableCapabilitiesByEntitySet } from './odata';
7676
export { hasDependency } from './project';

packages/project-access/src/library/helpers.ts

Lines changed: 106 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,81 @@ export function getManifestDependencies(manifest: Manifest): string[] {
311311
return result;
312312
}
313313

314+
/**
315+
* Recursively searches for an element with the specified id attribute in a parsed XML object.
316+
*
317+
* @param obj - parsed XML object to search in
318+
* @param id - id attribute value to search for
319+
* @param attrPrefix - attribute prefix used by the parser (default: '@_')
320+
* @returns true if an element with the specified id is found
321+
*/
322+
function hasElementWithId(obj: unknown, id: string, attrPrefix: string = '@_'): boolean {
323+
if (!obj || typeof obj !== 'object') {
324+
return false;
325+
}
326+
327+
const objRecord = obj as Record<string, unknown>;
328+
329+
// Check if current object has the id attribute
330+
if (objRecord[`${attrPrefix}id`] === id) {
331+
return true;
332+
}
333+
334+
// Recursively search in all properties
335+
for (const key in objRecord) {
336+
if (key.startsWith(attrPrefix)) {
337+
continue; // Skip attributes
338+
}
339+
340+
if (checkIdInValue(objRecord[key], id, attrPrefix)) {
341+
return true;
342+
}
343+
}
344+
345+
return false;
346+
}
347+
348+
/**
349+
* Checks if a value (object or array) contains an element with the specified id.
350+
*
351+
* @param value - value to check (can be array or object)
352+
* @param id - id to search for
353+
* @param attrPrefix - attribute prefix used by the parser
354+
* @returns true if id is found in the value
355+
*/
356+
function checkIdInValue(value: unknown, id: string, attrPrefix: string): boolean {
357+
if (Array.isArray(value)) {
358+
return value.some((item) => hasElementWithId(item, id, attrPrefix));
359+
}
360+
if (typeof value === 'object' && value !== null) {
361+
return hasElementWithId(value, id, attrPrefix);
362+
}
363+
return false;
364+
}
365+
366+
/**
367+
* Checks if an element with the specified id is available (does not exist) in the XML content.
368+
*
369+
* @param id - id to check for availability
370+
* @param xmlContent - XML content as string
371+
* @returns true if the id is available (not found), false if it exists
372+
*/
373+
function checkElementIdAvailable(id: string, xmlContent: string): boolean {
374+
const parser = new XMLParser({
375+
ignoreAttributes: false,
376+
attributeNamePrefix: '@_',
377+
parseAttributeValue: false
378+
});
379+
380+
try {
381+
const xmlDocument: unknown = parser.parse(xmlContent);
382+
return xmlDocument ? !hasElementWithId(xmlDocument, id) : true;
383+
} catch {
384+
// Parse error = no valid document = no element with id
385+
return true;
386+
}
387+
}
388+
314389
/**
315390
* Validates if an id is unique across XML files (fragments and views) in the project.
316391
* Synchronous overload - when files are provided directly.
@@ -347,99 +422,13 @@ export function validateId(
347422
options: { files?: never; appPath: string; memFs?: Editor }
348423
): Promise<boolean>;
349424

350-
/**
351-
* Validates if an id is unique across XML files (fragments and views) in the project.
352-
* Asynchronous overload - when no options are provided.
353-
*
354-
* @param baseId - id to validate
355-
* @param validatedIds - array of ids that are already validated/used
356-
* @param options - undefined (no validation options)
357-
* @returns Promise that resolves to true (always valid when no files to check)
358-
*/
359-
export function validateId(baseId: string, validatedIds?: string[], options?: undefined): Promise<boolean>;
360-
361425
// Implementation
362426
export function validateId(
363427
baseId: string,
364-
validatedIds?: string[],
365-
options?: { files?: string[]; appPath?: string; memFs?: Editor }
428+
validatedIds: string[] | undefined,
429+
options: { files?: string[]; appPath?: string; memFs?: Editor }
366430
): boolean | Promise<boolean> {
367-
const { memFs, appPath, files: fileContents } = options ?? {};
368-
369-
/**
370-
* Checks if an element with the specified id is available (does not exist) in the XML content.
371-
*
372-
* @param id - id to check for availability
373-
* @param xmlContent - XML content as string
374-
* @returns true if the id is available (not found), false if it exists
375-
*/
376-
function checkElementIdAvailable(id: string, xmlContent: string): boolean {
377-
const parser = new XMLParser({
378-
ignoreAttributes: false,
379-
attributeNamePrefix: '@_',
380-
parseAttributeValue: false
381-
});
382-
383-
try {
384-
const xmlDocument: unknown = parser.parse(xmlContent);
385-
return xmlDocument ? !hasElementWithId(xmlDocument, id) : true;
386-
} catch {
387-
// Parse error = no valid document = no element with id
388-
return true;
389-
}
390-
}
391-
392-
/**
393-
* Checks if a value (object or array) contains an element with the specified id.
394-
*
395-
* @param value - value to check (can be array or object)
396-
* @param id - id to search for
397-
* @param attrPrefix - attribute prefix used by the parser
398-
* @returns true if id is found in the value
399-
*/
400-
function checkIdInValue(value: unknown, id: string, attrPrefix: string): boolean {
401-
if (Array.isArray(value)) {
402-
return value.some((item) => hasElementWithId(item, id, attrPrefix));
403-
}
404-
if (typeof value === 'object' && value !== null) {
405-
return hasElementWithId(value, id, attrPrefix);
406-
}
407-
return false;
408-
}
409-
410-
/**
411-
* Recursively searches for an element with the specified id attribute in a parsed XML object.
412-
*
413-
* @param obj - parsed XML object to search in
414-
* @param id - id attribute value to search for
415-
* @param attrPrefix - attribute prefix used by the parser (default: '@_')
416-
* @returns true if an element with the specified id is found
417-
*/
418-
function hasElementWithId(obj: unknown, id: string, attrPrefix: string = '@_'): boolean {
419-
if (!obj || typeof obj !== 'object') {
420-
return false;
421-
}
422-
423-
const objRecord = obj as Record<string, unknown>;
424-
425-
// Check if current object has the id attribute
426-
if (objRecord[`${attrPrefix}id`] === id) {
427-
return true;
428-
}
429-
430-
// Recursively search in all properties
431-
for (const key in objRecord) {
432-
if (key.startsWith(attrPrefix)) {
433-
continue; // Skip attributes
434-
}
435-
436-
if (checkIdInValue(objRecord[key], id, attrPrefix)) {
437-
return true;
438-
}
439-
}
440-
441-
return false;
442-
}
431+
const { memFs, appPath, files: fileContents } = options;
443432

444433
/**
445434
* Validates the ID against the provided files.
@@ -448,42 +437,42 @@ export function validateId(
448437
* @returns true if the id is unique (available), false if it already exists
449438
*/
450439
function validateAgainstFiles(files: string[]): boolean {
451-
return (
452-
files.every((content) => content === '' || checkElementIdAvailable(baseId, content)) &&
453-
!validatedIds?.includes(baseId)
454-
);
440+
// Check validatedIds first - fast O(n) check avoids expensive XML parsing
441+
if (validatedIds?.includes(baseId)) {
442+
return false;
443+
}
444+
445+
// Only parse XML files if validatedIds check passed
446+
return files.every((content) => content === '' || checkElementIdAvailable(baseId, content));
455447
}
456448

457449
// Synchronous path: when files are provided directly
458450
if (fileContents !== undefined) {
459451
return validateAgainstFiles(fileContents);
460452
}
461453

462-
// Asynchronous path: when appPath is provided or no options
454+
// Asynchronous path: when appPath is provided
455+
if (!appPath) {
456+
throw new Error('validateId requires either files or appPath to be provided in options');
457+
}
458+
463459
return (async (): Promise<boolean> => {
464-
let files: string[] | undefined;
465-
if (appPath) {
466-
// Ensure we have a memFs instance
467-
const fsEditor = memFs ?? create(createStorage());
468-
469-
const xmlFilePaths = await findFilesByExtension(
470-
'.xml',
471-
appPath,
472-
['.git', 'node_modules', 'dist', 'annotations', 'localService'],
473-
fsEditor
474-
);
475-
const lookupFiles = ['.fragment.xml', '.view.xml'];
476-
const filteredPaths = xmlFilePaths.filter((fileName: string) =>
477-
lookupFiles.some((lookupFile) => fileName.endsWith(lookupFile))
478-
);
479-
480-
// Read file contents from paths using memFs
481-
files = filteredPaths.map((path: string) => fsEditor.read(path));
482-
}
460+
// Ensure we have a memFs instance
461+
const fsEditor = memFs ?? create(createStorage());
462+
463+
const xmlFilePaths = await findFilesByExtension(
464+
'.xml',
465+
appPath,
466+
['.git', 'node_modules', 'dist', 'annotations', 'localService'],
467+
fsEditor
468+
);
469+
const lookupFiles = ['.fragment.xml', '.view.xml'];
470+
const filteredPaths = xmlFilePaths.filter((fileName: string) =>
471+
lookupFiles.some((lookupFile) => fileName.endsWith(lookupFile))
472+
);
483473

484-
if (files) {
485-
return validateAgainstFiles(files);
486-
}
487-
return true;
474+
// Read file contents from paths using memFs
475+
const files = filteredPaths.map((path: string) => fsEditor.read(path));
476+
return validateAgainstFiles(files);
488477
})();
489478
}

0 commit comments

Comments
 (0)