Skip to content

Commit

Permalink
feat: During building block creation, add the 'sap.fe.macros' library…
Browse files Browse the repository at this point in the history
… to the 'manifest.json' if it is not already listed (#2348)

* feat: add macros library on building block creation

add macros library on building block creation

* fix: lint

lint

* fix: correction

correction

* changeset

changeset

* fix: request comment

request comment

* fix: snapshot

snapshot

* fix: examples

examples
  • Loading branch information
815are authored Sep 13, 2024
1 parent dd53a2e commit 86bcf45
Show file tree
Hide file tree
Showing 11 changed files with 805 additions and 181 deletions.
6 changes: 6 additions & 0 deletions .changeset/lucky-candles-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/fe-fpm-writer': patch
---

- During building block creation, add the 'sap.fe.macros' library to the 'manifest.json' if it is not already listed
- The API methods `generateBuildingBlock`, `getSerializedFileContent`, `PromptsAPI.submitAnswers`, and `PromptsAPI.getCodeSnippets` changed from synchronous to asynchronous.
6 changes: 3 additions & 3 deletions examples/fe-fpm-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function generateFilterBarBuildingBlock(fs: Editor): Promise<Editor
prompt.questions,
prompt.initialAnswers
)) as FilterBarPromptsAnswer;
fs = promptsAPI.submitAnswers(PromptsType.FilterBar, answers);
fs = await promptsAPI.submitAnswers(PromptsType.FilterBar, answers);
return fs;
}

Expand All @@ -61,7 +61,7 @@ export async function generateChartBuildingBlock(fs: Editor): Promise<Editor> {
prompt.questions,
prompt.initialAnswers
)) as ChartPromptsAnswer;
fs = promptsAPI.submitAnswers(PromptsType.Chart, answers);
fs = await promptsAPI.submitAnswers(PromptsType.Chart, answers);
return fs;
}
/**
Expand All @@ -78,7 +78,7 @@ export async function generateTableBuildingBlock(fs: Editor): Promise<Editor> {
prompt.questions,
prompt.initialAnswers
)) as TablePromptsAnswer;
fs = promptsAPI.submitAnswers(PromptsType.Table, answers);
fs = await promptsAPI.submitAnswers(PromptsType.Table, answers);
return fs;
}

Expand Down
4 changes: 2 additions & 2 deletions examples/prompting-ui/src/backend/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ async function handleAction(action: Actions): Promise<void> {
}
case APPLY_ANSWERS: {
const { answers, buildingBlockType } = action;
const _fs = promptsAPI.submitAnswers(buildingBlockType, answers);
const _fs = await promptsAPI.submitAnswers(buildingBlockType, answers);
await promisify(_fs.commit).call(_fs);
const responseAction: ResetAnswers = {
type: RESET_ANSWERS,
Expand All @@ -177,7 +177,7 @@ async function handleAction(action: Actions): Promise<void> {
}
case GET_CODE_SNIPPET: {
const { answers, buildingBlockType } = action;
const codeSnippets = promptsAPI.getCodeSnippets(buildingBlockType, answers);
const codeSnippets = await promptsAPI.getCodeSnippets(buildingBlockType, answers);
const responseAction: UpdateCodeSnippet = {
type: UPDATE_CODE_SNIPPET,
buildingBlockType,
Expand Down
48 changes: 37 additions & 11 deletions packages/fe-fpm-writer/src/building-block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { BuildingBlockType, type BuildingBlock, type BuildingBlockConfig, type B
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
import * as xpath from 'xpath';
import format from 'xml-formatter';
import { getErrorMessage, validateBasePath } from '../common/validate';
import { getErrorMessage, validateBasePath, validateDependenciesLibs } from '../common/validate';
import { getTemplatePath } from '../templates';
import { CodeSnippetLanguage, type FilePathProps, type CodeSnippet } from '../prompts/types';
import { coerce, lt } from 'semver';
import type { Manifest } from '../common/types';
import { getMinimumUI5Version } from '@sap-ux/project-access';
import { getMinimumUI5Version, getWebappPath } from '@sap-ux/project-access';
import { detectTabSpacing, extendJSON } from '../common/file';

const PLACEHOLDERS = {
'id': 'REPLACE_WITH_BUILDING_BLOCK_ID',
Expand All @@ -25,15 +26,26 @@ interface MetadataPath {
metaPath: string;
}

/**
* Gets manifest path.
*
* @param {string} basePath the base path
* @param {Editor} fs the memfs editor instance
* @returns {Manifest | undefined} path to manifest file
*/
async function getManifestPath(basePath: string, fs: Editor): Promise<string> {
return join(await getWebappPath(basePath, fs), 'manifest.json');
}

/**
* Gets manifest content.
*
* @param {string} basePath the base path
* @param {Editor} fs the memfs editor instance
* @returns {Manifest | undefined} the manifest content
*/
function getManifest(basePath: string, fs: Editor): Manifest | undefined {
const manifestPath = join(basePath, 'webapp/manifest.json');
async function getManifest(basePath: string, fs: Editor): Promise<Manifest | undefined> {
const manifestPath = await getManifestPath(basePath, fs);
return fs.readJSON(manifestPath) as Manifest;
}

Expand All @@ -45,26 +57,40 @@ function getManifest(basePath: string, fs: Editor): Manifest | undefined {
* @param {Editor} [fs] - the memfs editor instance
* @returns {Editor} the updated memfs editor instance
*/
export function generateBuildingBlock<T extends BuildingBlock>(
export async function generateBuildingBlock<T extends BuildingBlock>(
basePath: string,
config: BuildingBlockConfig<T>,
fs?: Editor
): Editor {
): Promise<Editor> {
// Validate the base and view paths
if (!fs) {
fs = create(createStorage());
}
validateBasePath(basePath, fs, ['sap.fe.templates', 'sap.fe.core']);
validateBasePath(basePath, fs, []);

if (!fs.exists(join(basePath, config.viewOrFragmentPath))) {
throw new Error(`Invalid view path ${config.viewOrFragmentPath}.`);
}

// Read the view xml and template files and update contents of the view xml file
const xmlDocument = getUI5XmlDocument(basePath, config.viewOrFragmentPath, fs);
const manifest = getManifest(basePath, fs);
const manifest = await getManifest(basePath, fs);
const templateDocument = getTemplateDocument(config.buildingBlockData, xmlDocument, fs, manifest);
fs = updateViewFile(basePath, config.viewOrFragmentPath, config.aggregationPath, xmlDocument, templateDocument, fs);

if (manifest && !validateDependenciesLibs(manifest, ['sap.fe.macros'])) {
// "sap.fe.macros" is missing - enhance manifest.json for missing "sap.fe.macros"
const manifestPath = await getManifestPath(basePath, fs);
const templatePath = getTemplatePath('/building-block/common/manifest.json');
const content = fs.read(manifestPath);
const tabInfo = detectTabSpacing(content);
extendJSON(fs, {
filepath: manifestPath,
content: render(fs.read(templatePath), { libraries: { 'sap.fe.macros': {} } }),
tabInfo: tabInfo
});
}

return fs;
}

Expand Down Expand Up @@ -310,11 +336,11 @@ function getFilePathProps(basePath: string, relativePath?: string): FilePathProp
* @param {Editor} [fs] - The memfs editor instance
* @returns {{ [questionName: string]: CodeSnippet }} An object with serialized code snippet content and file props
*/
export function getSerializedFileContent<T extends BuildingBlock>(
export async function getSerializedFileContent<T extends BuildingBlock>(
basePath: string,
config: BuildingBlockConfig<T>,
fs?: Editor
): { [questionName: string]: CodeSnippet } {
): Promise<{ [questionName: string]: CodeSnippet }> {
if (!config.buildingBlockData?.buildingBlockType) {
return {};
}
Expand All @@ -326,7 +352,7 @@ export function getSerializedFileContent<T extends BuildingBlock>(
const xmlDocument = config.viewOrFragmentPath
? getUI5XmlDocument(basePath, config.viewOrFragmentPath, fs)
: undefined;
const manifest = getManifest(basePath, fs);
const manifest = await getManifest(basePath, fs);
const content = getTemplateContent(config.buildingBlockData, xmlDocument, manifest, fs, true);
const filePathProps = getFilePathProps(basePath, config.viewOrFragmentPath);
return {
Expand Down
27 changes: 19 additions & 8 deletions packages/fe-fpm-writer/src/common/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { create as createStorage } from 'mem-fs';
import type { Editor } from 'mem-fs-editor';
import { create } from 'mem-fs-editor';
import { coerce, lt } from 'semver';
import type { Manifest } from './types';

/**
* Validate that the UI5 version requirement is valid.
Expand All @@ -18,6 +19,22 @@ export function validateVersion(ui5Version?: string): boolean {
return true;
}

/**
* Validates the library dependencies - at least one of expected dependencies is present.
*
* @param {string} manifest - the manifest content
* @param {string[]} dependencies - expected dependencies
* @returns true if at least one of expected dependencies is presented in manifest.
*/
export function validateDependenciesLibs(manifest: Manifest, dependencies: string[]): boolean {
const libs = manifest['sap.ui5']?.dependencies?.libs;
return dependencies.length
? dependencies.some((dependency) => {
return libs?.[dependency] !== undefined;
})
: true;
}

/**
* Validates the provided base path, checks at least one of expected dependencies is present.
*
Expand All @@ -35,14 +52,8 @@ export function validateBasePath(basePath: string, fs?: Editor, dependencies = [
if (!fs.exists(manifestPath)) {
throw new Error(`Invalid project folder. Cannot find required file ${manifestPath}`);
} else {
const manifest = fs.readJSON(manifestPath) as any;
const libs = manifest['sap.ui5']?.dependencies?.libs;
const valid = dependencies.length
? dependencies.some((dependency) => {
return libs?.[dependency] !== undefined;
})
: true;
if (!valid) {
const manifest = fs.readJSON(manifestPath) as Manifest;
if (!validateDependenciesLibs(manifest, dependencies)) {
if (dependencies.length === 1) {
throw new Error(
`Dependency ${dependencies[0]} is missing in the manifest.json. Fiori elements FPM requires the SAP FE libraries.`
Expand Down
8 changes: 4 additions & 4 deletions packages/fe-fpm-writer/src/prompts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ export class PromptsAPI {
* @param answers The answers object
* @returns The updated memfs editor instance
*/
public submitAnswers<N extends SupportedPrompts['type']>(
public async submitAnswers<N extends SupportedPrompts['type']>(
type: N,
answers: NarrowPrompt<typeof type>['answers']
): Editor {
): Promise<Editor> {
const config = { type, answers };
if (!this.isGenerationSupported(config)) {
return this.context.fs;
Expand All @@ -183,10 +183,10 @@ export class PromptsAPI {
* @param answers The answers object
* @returns Code snippet content.
*/
public getCodeSnippets<N extends SupportedPrompts['type']>(
public async getCodeSnippets<N extends SupportedPrompts['type']>(
type: N,
answers: NarrowPrompt<typeof type>['answers']
): { [questionName: string]: CodeSnippet } {
): Promise<{ [questionName: string]: CodeSnippet }> {
const config = { type, answers };
if (!this.isGenerationSupported(config)) {
return {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sap.ui5": {
"dependencies": {
"libs": <%- JSON.stringify(libraries) %>
}
}
}
14 changes: 7 additions & 7 deletions packages/fe-fpm-writer/test/integration/custom-page-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Test FPM features using a pre-generated Fiori Custom Page app', () =>
return writeFilesForDebugging(fs);
});

describe('generate building blocks', () => {
describe('generate building blocks', async () => {
const basicConfig = {
path: join(testOutput, 'js')
};
Expand All @@ -32,8 +32,8 @@ describe('Test FPM features using a pre-generated Fiori Custom Page app', () =>
fs.copy(join(testInput, 'ts'), tsConfig.path, { globOptions: { dot: true } });
});

test.each(configs)('generateBuildingBlock:FilterBar in custom page', (config) => {
generateBuildingBlock<FilterBar>(
test.each(configs)('generateBuildingBlock:FilterBar in custom page', async (config) => {
await generateBuildingBlock<FilterBar>(
config.path,
{
viewOrFragmentPath: join('webapp/ext/main/Main.view.xml'),
Expand All @@ -48,8 +48,8 @@ describe('Test FPM features using a pre-generated Fiori Custom Page app', () =>
);
});

test.each(configs)('generateBuildingBlock:Chart in custom page', (config) => {
generateBuildingBlock<Chart>(
test.each(configs)('generateBuildingBlock:Chart in custom page', async (config) => {
await generateBuildingBlock<Chart>(
config.path,
{
viewOrFragmentPath: join('webapp/ext/main/Main.view.xml'),
Expand All @@ -67,8 +67,8 @@ describe('Test FPM features using a pre-generated Fiori Custom Page app', () =>
);
});

test.each(configs)('generateBuildingBlock:Table in custom page', (config) => {
generateBuildingBlock<Table>(
test.each(configs)('generateBuildingBlock:Table in custom page', async (config) => {
await generateBuildingBlock<Table>(
config.path,
{
viewOrFragmentPath: join('webapp/ext/main/Main.view.xml'),
Expand Down
Loading

0 comments on commit 86bcf45

Please sign in to comment.