Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
41693c5
support reading path mappings for other types than application
heimwege Nov 25, 2025
9b401af
Linting auto fix commit
github-actions[bot] Nov 25, 2025
6373487
export getPathMappings
heimwege Nov 26, 2025
cceda75
add unit tests
heimwege Nov 26, 2025
41558de
add cset
heimwege Nov 26, 2025
2a1486b
rephrase JSDoc
heimwege Nov 26, 2025
66147aa
Linting auto fix commit
github-actions[bot] Nov 26, 2025
54ee1f1
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Nov 26, 2025
4cc4e80
Merge branch 'main' into feat/project-access/support-reading-path-map…
johannes-kolbe Nov 28, 2025
26aabb8
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 1, 2025
2c04d06
refactoring
heimwege Dec 2, 2025
1201473
Linting auto fix commit
github-actions[bot] Dec 2, 2025
079ae3a
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 2, 2025
427049d
test: comment out test
heimwege Dec 3, 2025
b785579
Merge branch 'feat/project-access/support-reading-path-mappings-for-o…
heimwege Dec 3, 2025
e6948c7
undo: comment out test
heimwege Dec 3, 2025
47dd24a
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 3, 2025
074bf68
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 3, 2025
3686325
fix unit tests
heimwege Dec 3, 2025
89c51ed
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 3, 2025
ff0daec
Update JSDoc
heimwege Dec 3, 2025
2897ac8
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 4, 2025
328efce
Merge branch 'main' into feat/project-access/support-reading-path-map…
Klaus-Keller Dec 4, 2025
7590480
Merge branch 'main' into feat/project-access/support-reading-path-map…
heimwege Dec 4, 2025
8075db5
tiny fix
heimwege Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/three-rocks-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/project-access': patch
---

feat: support reading path mappings for other types than application
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ }
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('package-json', () => {
const variousConfigsPath = join(basePath, 'various-configs');

const variousConfigsPackageJsonPath = join(variousConfigsPath, 'package.json');
fs.delete(variousConfigsPackageJsonPath);
ensurePreviewMiddlewareDependency(fs, variousConfigsPath);
expect(() => fs.read(join(variousConfigsPath, 'package.json'))).toThrow(
`${variousConfigsPackageJsonPath} doesn\'t exist`
Expand Down
1 change: 1 addition & 0 deletions packages/project-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {
getMockServerConfig,
getMockDataPath,
getNodeModulesPath,
getPathMappings,
getProject,
getProjectType,
getWebappPath,
Expand Down
9 changes: 8 additions & 1 deletion packages/project-access/src/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ export {
findCapProjectRoot,
findRootsForPath
} from './search';
export { getWebappPath, readUi5Yaml, getAllUi5YamlFileNames, getMockServerConfig, getMockDataPath } from './ui5-config';
export {
getWebappPath,
readUi5Yaml,
getAllUi5YamlFileNames,
getMockServerConfig,
getMockDataPath,
getPathMappings
} from './ui5-config';
export { getMtaPath } from './mta';
export { createApplicationAccess, createProjectAccess } from './access';
export { updatePackageScript, hasUI5CliV3 } from './script';
Expand Down
88 changes: 73 additions & 15 deletions packages/project-access/src/project/ui5-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import { basename, dirname, join } from 'node:path';
import type { Editor } from 'mem-fs-editor';
import type { MockserverConfig, MockserverService } from '@sap-ux/ui5-config';
import type { MockserverConfig, MockserverService, Ui5Document, Configuration } from '@sap-ux/ui5-config';
import { UI5Config } from '@sap-ux/ui5-config';
import { DirName, FileName } from '../constants';
import { fileExists, findFilesByExtension, findFileUp, readFile } from '../file';

type PathMappings = { [key: string]: string | undefined };

const PATH_MAPPING_DEFAULTS: Record<Ui5Document['type'], Record<string, string>> = {
application: { webapp: DirName.Webapp },
library: { src: 'src', test: 'test' },
'theme-library': { src: 'src', test: 'test' },
module: {}
};

/**
* Get base directory of the project where package.json is located.
*
* @param appRoot - root to the application
* @param memFs - optional mem-fs editor instance
* @returns - base directory of the project
*/
async function getBaseDir(appRoot: string, memFs?: Editor): Promise<string> {
const packageJsonPath = await findFileUp(FileName.Package, appRoot, memFs);
return packageJsonPath ? dirname(packageJsonPath) : appRoot;
}

/**
* Get path to webapp.
*
Expand All @@ -13,22 +34,59 @@ import { fileExists, findFilesByExtension, findFileUp, readFile } from '../file'
* @returns - path to webapp folder
*/
export async function getWebappPath(appRoot: string, memFs?: Editor): Promise<string> {
const ui5YamlPath = join(appRoot, FileName.Ui5Yaml);
let webappPath = join(appRoot, DirName.Webapp);
if (await fileExists(ui5YamlPath, memFs)) {
const yamlString = await readFile(ui5YamlPath, memFs);
const ui5Config = await UI5Config.newInstance(yamlString);
const relativeWebappPath = ui5Config.getConfiguration()?.paths?.webapp;
if (relativeWebappPath) {
// Search for folder with package.json inside
const packageJsonPath = await findFileUp(FileName.Package, appRoot, memFs);
if (packageJsonPath) {
const packageJsonDirPath = dirname(packageJsonPath);
webappPath = join(packageJsonDirPath, relativeWebappPath);
}
let pathMappings: PathMappings = {};
try {
pathMappings = await getPathMappings(appRoot, memFs);
} catch {
// For backward compatibility ignore errors and use default
}
return pathMappings?.webapp ?? join(appRoot, DirName.Webapp);
}

/**
* Get path mappings defined in 'ui5.yaml' depending on the project type defined in 'ui5.yaml'.
*
* @param appRoot - root to the application
* @param memFs - optional mem-fs editor instance
* @param fileName - optional name of yaml file to be read. Defaults to 'ui5.yaml'.
* @returns - path mappings
* @throws {Error} if ui5.yaml or 'type' cannot be read
* @throws {Error} if project type is not 'application', 'library', 'theme-library' or 'module'
*/
export async function getPathMappings(
appRoot: string,
memFs?: Editor,
fileName: string = FileName.Ui5Yaml
): Promise<PathMappings> {
let ui5Config: UI5Config;
let configuration: Configuration;
let type: Ui5Document['type'];
try {
ui5Config = await readUi5Yaml(appRoot, fileName, memFs);
configuration = ui5Config.getConfiguration();
type = ui5Config.getType();
} catch {
throw new Error(`Could not read 'type' from ${fileName} in project root: ${appRoot}`);
}

if (!(type in PATH_MAPPING_DEFAULTS)) {
throw new Error(`Unsupported project type for path mappings: ${type}`);
}

const baseDir = await getBaseDir(appRoot, memFs);
const pathMappings: PathMappings = {};
for (const [key, value] of Object.entries(configuration?.paths || {})) {
pathMappings[key] = join(baseDir, value ?? PATH_MAPPING_DEFAULTS[type][key]);
}

//Add defaults if no specific value exists
for (const [key, defaultValue] of Object.entries(PATH_MAPPING_DEFAULTS[type] ?? {})) {
if (!pathMappings[key]) {
pathMappings[key] = join(baseDir, defaultValue);
}
}
return webappPath;

return pathMappings;
}

/**
Expand Down
119 changes: 117 additions & 2 deletions packages/project-access/test/project/ui5-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getAllUi5YamlFileNames,
getMockDataPath,
getMockServerConfig,
getPathMappings,
getWebappPath,
readUi5Yaml
} from '../../src';
Expand Down Expand Up @@ -79,7 +80,7 @@ describe('Test getWebappPath()', () => {
const memFs = create(createStorage());
memFs.write(
join(samplesRoot, 'custom-webapp-path/ui5.yaml'),
'resources:\n configuration:\n paths:\n webapp: new/webapp/path'
'type: application\nresources:\n configuration:\n paths:\n webapp: new/webapp/path'
);
memFs.writeJSON(join(samplesRoot, 'custom-webapp-path/package.json'), {});
expect(await getWebappPath(join(samplesRoot, 'custom-webapp-path'), memFs)).toEqual(
Expand All @@ -91,7 +92,7 @@ describe('Test getWebappPath()', () => {
const memFs = create(createStorage());
memFs.write(
join(samplesRoot, 'app/app1/ui5.yaml'),
'resources:\n configuration:\n paths:\n webapp: app/app1/webapp'
'type: application\nresources:\n configuration:\n paths:\n webapp: app/app1/webapp'
);
memFs.writeJSON(join(samplesRoot, 'package.json'), {});
expect(await getWebappPath(join(samplesRoot, 'app/app1'), memFs)).toEqual(join(samplesRoot, 'app/app1/webapp'));
Expand All @@ -114,6 +115,7 @@ describe('Test readUi5Yaml()', () => {
},
},
},
"type": "application",
},
],
},
Expand Down Expand Up @@ -195,6 +197,119 @@ describe('Test readUi5Yaml()', () => {
});
});

describe('Test getPathMappings()', () => {
const samplesRoot = join(__dirname, '..', 'test-data', 'project', 'path-mappings');

describe('Application type projects', () => {
test('Get path mappings from default application', async () => {
const result = await getPathMappings(join(samplesRoot, 'default-application'));
expect(result).toEqual({
webapp: join(samplesRoot, 'default-application', 'webapp')
});
});

test('Get path mappings from application with custom webapp mapping', async () => {
const result = await getPathMappings(join(samplesRoot, 'custom-application'));
expect(result).toEqual({
webapp: join(samplesRoot, 'custom-application', 'src', 'main', 'webapp')
});
});

test('Get custom webapp path mappings from mem-fs editor instance', async () => {
const memFs = create(createStorage());
const ui5YamlPath = join(samplesRoot, 'custom-application/ui5.yaml');
const ui5YamlContent = await readFile(ui5YamlPath, 'utf-8');
memFs.write(ui5YamlPath, ui5YamlContent);
memFs.writeJSON(join(samplesRoot, 'custom-application/package.json'), {});
const result = await getPathMappings(join(samplesRoot, 'custom-application'), memFs);
expect(result).toEqual({
webapp: join(samplesRoot, 'custom-application/src/main/webapp')
});
});
});

describe('Library type projects', () => {
test('Get path mappings from default library', async () => {
const result = await getPathMappings(join(samplesRoot, 'default-library'));
expect(result).toEqual({
src: join(samplesRoot, 'default-library', 'src'),
test: join(samplesRoot, 'default-library', 'test')
});
});

test('Get path mappings from library with custom src and test mappings', async () => {
const result = await getPathMappings(join(samplesRoot, 'custom-library'));
expect(result).toEqual({
src: join(samplesRoot, 'custom-library', 'custom', 'src'),
test: join(samplesRoot, 'custom-library', 'custom', 'test')
});
});

test('Get custom library path mappings from mem-fs editor instance', async () => {
const memFs = create(createStorage());
const ui5YamlPath = join(samplesRoot, 'custom-library/ui5.yaml');
const ui5YamlContent = await readFile(ui5YamlPath, 'utf-8');
memFs.write(ui5YamlPath, ui5YamlContent);
memFs.writeJSON(join(samplesRoot, 'custom-library/package.json'), {});
const result = await getPathMappings(join(samplesRoot, 'custom-library'), memFs);
expect(result).toEqual({
src: join(samplesRoot, 'custom-library/custom/src'),
test: join(samplesRoot, 'custom-library/custom/test')
});
});

test('Get path mappings from library with partial custom paths (only src)', async () => {
const memFs = create(createStorage());
const ui5YamlPath = join(samplesRoot, 'default-library/ui5.yaml');
const ui5YamlContent = await readFile(ui5YamlPath, 'utf-8');
const modifiedContent =
ui5YamlContent + 'resources:\n configuration:\n paths:\n src: custom/src\n';
memFs.write(ui5YamlPath, modifiedContent);
memFs.writeJSON(join(samplesRoot, 'default-library/package.json'), {});
const result = await getPathMappings(join(samplesRoot, 'default-library'), memFs);
expect(result).toEqual({
src: join(samplesRoot, 'default-library/custom/src'),
test: join(samplesRoot, 'default-library', 'test')
});
});

test('Get path mappings from library with partial custom paths (only test)', async () => {
const memFs = create(createStorage());
const ui5YamlPath = join(samplesRoot, 'default-library/ui5.yaml');
const ui5YamlContent = await readFile(ui5YamlPath, 'utf-8');
const modifiedContent =
ui5YamlContent + 'resources:\n configuration:\n paths:\n test: custom/test\n';
memFs.write(ui5YamlPath, modifiedContent);
memFs.writeJSON(join(samplesRoot, 'default-library/package.json'), {});
const result = await getPathMappings(join(samplesRoot, 'default-library'), memFs);
expect(result).toEqual({
src: join(samplesRoot, 'default-library', 'src'),
test: join(samplesRoot, 'default-library/custom/test')
});
});
});

describe('Edge cases', () => {
test('Return undefined when ui5.yaml does not exist', async () => {
await expect(getPathMappings(samplesRoot)).rejects.toThrow(
`Could not read 'type' from ui5.yaml in project root: ${samplesRoot}`
);
});

test('Return undefined for unsupported project type', async () => {
const memFs = create(createStorage());
memFs.write(
join(samplesRoot, 'no-ui5-yaml/ui5.yaml'),
'specVersion: "3.0"\ntype: unknown\nmetadata:\n name: test.unknown'
);
memFs.writeJSON(join(samplesRoot, 'no-ui5-yaml/package.json'), {});
await expect(getPathMappings(join(samplesRoot, 'no-ui5-yaml'), memFs)).rejects.toThrow(
'Unsupported project type for path mappings: unknown'
);
});
});
});

describe('get configuration for sap-fe-mockserver', () => {
const samplesRoot = join(__dirname, '..', 'test-data', 'project', 'webapp-path');
const projectPath = join(samplesRoot, 'default-with-ui5-yaml');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
type: application
resources:
configuration:
paths:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-custom-application"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
specVersion: "3.0"
type: application
metadata:
name: test.custom.application
resources:
configuration:
paths:
webapp: src/main/webapp

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-custom-library"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
specVersion: "3.0"
type: library
metadata:
name: test.custom.library
resources:
configuration:
paths:
src: custom/src
test: custom/test

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-default-application"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
specVersion: "3.0"
type: application
metadata:
name: test.default.application
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-default-library"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
specVersion: "3.0"
type: library
metadata:
name: test.default.library

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "test-no-ui5-yaml"
}

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
type: application
resources:
configuration:
paths:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type: application
resources:
configuration:
paths:
webapp: src/webapp
configuration:
paths:
webapp: src/webapp
Loading