Skip to content

Commit 45e087a

Browse files
refactor(cf-deploy-config-writer): consolidate disk-write template rendering (#4458) (#4462)
* refactor(cf-deploy-config-writer): consolidate disk-write template rendering Extract readFileSync+render+writeFileSync into renderTemplateToDisk() in template-renderer.ts. Update createMTA, createCAPMTAAppFrontend, and addMtaExtensionConfig to use it. Replace the hardcoded __dirname-relative path in mta.ts with getTemplatePath() for consistent resolution. * Linting auto fix commit * refactor(cf-deploy-config-writer): suppress S7790 for controlled template render Template is read from a known internal path via getTemplatePath(), not from user input — NOSONAR suppression is warranted. * refactor(cf-deploy-config-writer): address review feedback - Remove NOSONAR comment from template-renderer.ts (to be accepted in SonarCloud UI) - Trim changeset message to a single concise line --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 607005b commit 45e087a

5 files changed

Lines changed: 101 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/cf-deploy-config-writer': patch
3+
---
4+
5+
refactor(cf-deploy-config-writer): consolidate disk-write template rendering into a single module

packages/cf-deploy-config-writer/src/mta-config/index.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { readFileSync, writeFileSync } from 'node:fs';
21
import { join } from 'node:path';
3-
import { render } from 'ejs';
42
import { MtaConfig } from './mta';
3+
import { renderTemplateToDisk } from './template-renderer';
54
import {
65
addXSSecurityConfig,
76
getTemplatePath,
@@ -84,15 +83,13 @@ export function toMtaModuleName(appId: string): string {
8483
*/
8584
export function createMTA(config: MTABaseConfig): void {
8685
const mtaId = `${config.mtaId.slice(0, MAX_MTA_ID_LENGTH)}`;
87-
const mtaTemplate = readFileSync(getTemplatePath(`app/${FileName.MtaYaml}`), 'utf-8');
88-
const mtaContents = render(mtaTemplate, {
86+
config.mtaId = mtaId;
87+
// Written to disk immediately! Subsequent calls are dependent on it being on the file system i.e mta-lib.
88+
renderTemplateToDisk(`app/${FileName.MtaYaml}`, join(config.mtaPath, FileName.MtaYaml), {
8989
id: mtaId,
9090
mtaDescription: config.mtaDescription ?? MTADescription,
9191
mtaVersion: config.mtaVersion ?? MTAVersion
9292
});
93-
config.mtaId = mtaId;
94-
// Written to disk immediately! Subsequent calls are dependent on it being on the file system i.e mta-lib.
95-
writeFileSync(join(config.mtaPath, FileName.MtaYaml), mtaContents);
9693
LoggerHelper.logger?.debug(t('debug.mtaCreated', { mtaPath: config.mtaPath }));
9794
}
9895

@@ -162,14 +159,12 @@ export function validateMtaConfig(config: CFBaseConfig): void {
162159
* @deprecated This function is deprecated and will be removed in future releases
163160
*/
164161
async function createCAPMTAAppFrontend(config: CAPConfig, fs: Editor): Promise<void> {
165-
const mtaTemplate = readFileSync(getTemplatePath(`frontend/${FileName.MtaYaml}`), 'utf-8');
166-
const mtaContents = render(mtaTemplate, {
162+
// Written to disk immediately! Subsequent calls are dependent on it being on the file system i.e mta-lib.
163+
renderTemplateToDisk(`frontend/${FileName.MtaYaml}`, join(config.mtaPath, FileName.MtaYaml), {
167164
id: `${config.mtaId.slice(0, MAX_MTA_ID_LENGTH)}`,
168165
mtaDescription: config.mtaDescription ?? MTADescription,
169166
mtaVersion: config.mtaVersion ?? MTAVersion
170167
});
171-
// Written to disk immediately! Subsequent calls are dependent on it being on the file system i.e mta-lib.
172-
writeFileSync(join(config.mtaPath, FileName.MtaYaml), mtaContents);
173168
// Add missing configurations
174169
addXSSecurityConfig(config, fs, false);
175170
LoggerHelper.logger?.debug(t('debug.mtaCreated', { mtaPath: config.mtaPath }));

packages/cf-deploy-config-writer/src/mta-config/mta.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { format } from 'node:util';
22
import { dirname, join } from 'node:path';
33
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4-
import { render } from 'ejs';
54
import { Mta, type mta } from '@sap/mta-lib';
65
import { type Destination, isGenericODataDestination, isAbapEnvironmentOnBtp } from '@sap-ux/btp-utils';
76
import { YamlDocument } from '@sap-ux/yaml';
@@ -46,6 +45,7 @@ import {
4645
type SupportedResources,
4746
RouterModuleType
4847
} from '../types';
48+
import { renderTemplateToDisk } from './template-renderer';
4949

5050
/**
5151
* A class representing interactions with the MTA binary, found at https://sap.github.io/cloud-mta-build-tool/.
@@ -906,8 +906,7 @@ export class MtaConfig {
906906
destinationServiceName: destinationServiceName,
907907
mtaVersion: '1.0.0'
908908
};
909-
const mtaExtTemplate = readFileSync(join(__dirname, `../../templates/app/${FileName.MtaExtYaml}`), 'utf-8');
910-
writeFileSync(mtaExtFilePath, render(mtaExtTemplate, mtaExt));
909+
renderTemplateToDisk(`app/${FileName.MtaExtYaml}`, mtaExtFilePath, mtaExt);
911910
this.log?.info(t('info.mtaExtensionCreated', { appMtaId, mtaExtFile: FileName.MtaExtYaml }));
912911
} else {
913912
// Create an entry in an existing mta extension file
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { readFileSync, writeFileSync } from 'node:fs';
2+
import { render } from 'ejs';
3+
import { getTemplatePath } from '../utils';
4+
5+
/**
6+
* Render an EJS template directly to disk.
7+
* Intentionally bypasses mem-fs — mta-lib and other consumers require the file to be
8+
* physically present on the file system before they can read it back.
9+
*
10+
* @param templateName Template path relative to the `templates/` folder (e.g. `app/mta.yaml`)
11+
* @param outputPath Absolute path where the rendered file will be written
12+
* @param data Template data object passed to EJS
13+
*/
14+
export function renderTemplateToDisk(templateName: string, outputPath: string, data: Record<string, unknown>): void {
15+
const template = readFileSync(getTemplatePath(templateName), 'utf-8');
16+
writeFileSync(outputPath, render(template, data));
17+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { join } from 'node:path';
2+
import * as nodeFs from 'node:fs';
3+
import * as ejs from 'ejs';
4+
import * as utils from '../../src/utils';
5+
import { renderTemplateToDisk } from '../../src/mta-config/template-renderer';
6+
7+
jest.mock('node:fs', () => ({
8+
readFileSync: jest.fn(),
9+
writeFileSync: jest.fn()
10+
}));
11+
12+
jest.mock('ejs', () => ({
13+
render: jest.fn()
14+
}));
15+
16+
jest.mock('../../src/utils', () => ({
17+
getTemplatePath: jest.fn()
18+
}));
19+
20+
describe('renderTemplateToDisk', () => {
21+
const getTemplatePathMock = utils.getTemplatePath as jest.MockedFunction<typeof utils.getTemplatePath>;
22+
const readFileSyncMock = nodeFs.readFileSync as jest.MockedFunction<typeof nodeFs.readFileSync>;
23+
const writeFileSyncMock = nodeFs.writeFileSync as jest.MockedFunction<typeof nodeFs.writeFileSync>;
24+
const renderMock = ejs.render as jest.MockedFunction<typeof ejs.render>;
25+
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
30+
test('resolves template path, renders, and writes to disk', () => {
31+
const templateName = 'app/mta.yaml';
32+
const outputPath = '/project/mta.yaml';
33+
const data = { id: 'my-mta', mtaVersion: '0.0.1' };
34+
const resolvedTemplatePath = '/dist/templates/app/mta.yaml';
35+
const rawTemplate = '<%= id %>';
36+
const rendered = 'my-mta';
37+
38+
getTemplatePathMock.mockReturnValue(resolvedTemplatePath);
39+
readFileSyncMock.mockReturnValue(rawTemplate as any);
40+
renderMock.mockReturnValue(rendered);
41+
42+
renderTemplateToDisk(templateName, outputPath, data);
43+
44+
expect(getTemplatePathMock).toHaveBeenCalledWith(templateName);
45+
expect(readFileSyncMock).toHaveBeenCalledWith(resolvedTemplatePath, 'utf-8');
46+
expect(renderMock).toHaveBeenCalledWith(rawTemplate, data);
47+
expect(writeFileSyncMock).toHaveBeenCalledWith(outputPath, rendered);
48+
});
49+
50+
test('passes all data properties to the template renderer', () => {
51+
const data = { id: 'mta-id', mtaDescription: 'desc', mtaVersion: '1.0.0' };
52+
getTemplatePathMock.mockReturnValue('/tmpl');
53+
readFileSyncMock.mockReturnValue('tmpl' as any);
54+
renderMock.mockReturnValue('rendered');
55+
56+
renderTemplateToDisk('some/template.yaml', '/out.yaml', data);
57+
58+
expect(renderMock).toHaveBeenCalledWith('tmpl', data);
59+
});
60+
61+
test('uses output path directly for writeFileSync', () => {
62+
const outputPath = join('/nested', 'deep', 'output.yaml');
63+
getTemplatePathMock.mockReturnValue('/tmpl');
64+
readFileSyncMock.mockReturnValue('t' as any);
65+
renderMock.mockReturnValue('out');
66+
67+
renderTemplateToDisk('t', outputPath, {});
68+
69+
expect(writeFileSyncMock).toHaveBeenCalledWith(outputPath, 'out');
70+
});
71+
});

0 commit comments

Comments
 (0)