diff --git a/.changeset/three-rocks-change.md b/.changeset/three-rocks-change.md new file mode 100644 index 00000000000..315d3edc0f7 --- /dev/null +++ b/.changeset/three-rocks-change.md @@ -0,0 +1,5 @@ +--- +'@sap-ux/project-access': patch +--- + +feat: support reading path mappings for other types than application diff --git a/packages/app-config-writer/test/fixtures/preview-config/various-configs/package.json b/packages/app-config-writer/test/fixtures/preview-config/various-configs/package.json new file mode 100644 index 00000000000..6f31cf5a2e6 --- /dev/null +++ b/packages/app-config-writer/test/fixtures/preview-config/various-configs/package.json @@ -0,0 +1 @@ +{ } \ No newline at end of file diff --git a/packages/app-config-writer/test/unit/preview-config/package-json.test.ts b/packages/app-config-writer/test/unit/preview-config/package-json.test.ts index 70c5b647b07..c60b8c0d952 100644 --- a/packages/app-config-writer/test/unit/preview-config/package-json.test.ts +++ b/packages/app-config-writer/test/unit/preview-config/package-json.test.ts @@ -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` diff --git a/packages/project-access/src/index.ts b/packages/project-access/src/index.ts index b7d283a8d18..e90437d4b9d 100644 --- a/packages/project-access/src/index.ts +++ b/packages/project-access/src/index.ts @@ -37,6 +37,7 @@ export { getMockServerConfig, getMockDataPath, getNodeModulesPath, + getPathMappings, getProject, getProjectType, getWebappPath, diff --git a/packages/project-access/src/project/index.ts b/packages/project-access/src/project/index.ts index 6d7461e263a..5911b95bd1b 100644 --- a/packages/project-access/src/project/index.ts +++ b/packages/project-access/src/project/index.ts @@ -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'; diff --git a/packages/project-access/src/project/ui5-config.ts b/packages/project-access/src/project/ui5-config.ts index 88f5ea03439..8b5bbc1946a 100644 --- a/packages/project-access/src/project/ui5-config.ts +++ b/packages/project-access/src/project/ui5-config.ts @@ -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> = { + 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 { + const packageJsonPath = await findFileUp(FileName.Package, appRoot, memFs); + return packageJsonPath ? dirname(packageJsonPath) : appRoot; +} + /** * Get path to webapp. * @@ -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 { - 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 { + 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; } /** diff --git a/packages/project-access/test/project/ui5-config.test.ts b/packages/project-access/test/project/ui5-config.test.ts index ceffa90958b..76d3bfb5948 100644 --- a/packages/project-access/test/project/ui5-config.test.ts +++ b/packages/project-access/test/project/ui5-config.test.ts @@ -6,6 +6,7 @@ import { getAllUi5YamlFileNames, getMockDataPath, getMockServerConfig, + getPathMappings, getWebappPath, readUi5Yaml } from '../../src'; @@ -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( @@ -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')); @@ -114,6 +115,7 @@ describe('Test readUi5Yaml()', () => { }, }, }, + "type": "application", }, ], }, @@ -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'); diff --git a/packages/project-access/test/test-data/project/info/cap-project/apps/one/ui5.yaml b/packages/project-access/test/test-data/project/info/cap-project/apps/one/ui5.yaml index 25543f81d30..7d888eba9f3 100644 --- a/packages/project-access/test/test-data/project/info/cap-project/apps/one/ui5.yaml +++ b/packages/project-access/test/test-data/project/info/cap-project/apps/one/ui5.yaml @@ -1,3 +1,4 @@ +type: application resources: configuration: paths: diff --git a/packages/project-access/test/test-data/project/path-mappings/custom-application/package.json b/packages/project-access/test/test-data/project/path-mappings/custom-application/package.json new file mode 100644 index 00000000000..7e0eb30f4b6 --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/custom-application/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-custom-application" +} + diff --git a/packages/project-access/test/test-data/project/path-mappings/custom-application/ui5.yaml b/packages/project-access/test/test-data/project/path-mappings/custom-application/ui5.yaml new file mode 100644 index 00000000000..2509bccf68c --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/custom-application/ui5.yaml @@ -0,0 +1,9 @@ +specVersion: "3.0" +type: application +metadata: + name: test.custom.application +resources: + configuration: + paths: + webapp: src/main/webapp + diff --git a/packages/project-access/test/test-data/project/path-mappings/custom-library/package.json b/packages/project-access/test/test-data/project/path-mappings/custom-library/package.json new file mode 100644 index 00000000000..de7a623bd8c --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/custom-library/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-custom-library" +} + diff --git a/packages/project-access/test/test-data/project/path-mappings/custom-library/ui5.yaml b/packages/project-access/test/test-data/project/path-mappings/custom-library/ui5.yaml new file mode 100644 index 00000000000..9cf9dd8752b --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/custom-library/ui5.yaml @@ -0,0 +1,10 @@ +specVersion: "3.0" +type: library +metadata: + name: test.custom.library +resources: + configuration: + paths: + src: custom/src + test: custom/test + diff --git a/packages/project-access/test/test-data/project/path-mappings/default-application/package.json b/packages/project-access/test/test-data/project/path-mappings/default-application/package.json new file mode 100644 index 00000000000..ca70a8e9082 --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/default-application/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-default-application" +} + diff --git a/packages/project-access/test/test-data/project/path-mappings/default-application/ui5.yaml b/packages/project-access/test/test-data/project/path-mappings/default-application/ui5.yaml new file mode 100644 index 00000000000..adf8d1092ba --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/default-application/ui5.yaml @@ -0,0 +1,4 @@ +specVersion: "3.0" +type: application +metadata: + name: test.default.application diff --git a/packages/project-access/test/test-data/project/path-mappings/default-library/package.json b/packages/project-access/test/test-data/project/path-mappings/default-library/package.json new file mode 100644 index 00000000000..ede98cb74db --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/default-library/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-default-library" +} + diff --git a/packages/project-access/test/test-data/project/path-mappings/default-library/ui5.yaml b/packages/project-access/test/test-data/project/path-mappings/default-library/ui5.yaml new file mode 100644 index 00000000000..50183f1985d --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/default-library/ui5.yaml @@ -0,0 +1,5 @@ +specVersion: "3.0" +type: library +metadata: + name: test.default.library + diff --git a/packages/project-access/test/test-data/project/path-mappings/no-ui5-yaml/package.json b/packages/project-access/test/test-data/project/path-mappings/no-ui5-yaml/package.json new file mode 100644 index 00000000000..4534a4d64b6 --- /dev/null +++ b/packages/project-access/test/test-data/project/path-mappings/no-ui5-yaml/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-no-ui5-yaml" +} + diff --git a/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path-multi-yaml/ui5.yaml b/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path-multi-yaml/ui5.yaml index 9c8577b4401..2743d7c9c47 100644 --- a/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path-multi-yaml/ui5.yaml +++ b/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path-multi-yaml/ui5.yaml @@ -1,3 +1,4 @@ +type: application resources: configuration: paths: diff --git a/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path/ui5.yaml b/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path/ui5.yaml index c3749ab26f0..d85583ed13f 100644 --- a/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path/ui5.yaml +++ b/packages/project-access/test/test-data/project/webapp-path/custom-webapp-path/ui5.yaml @@ -1,4 +1,5 @@ +type: application resources: - configuration: - paths: - webapp: src/webapp + configuration: + paths: + webapp: src/webapp