@sap-ux/fe-fpm-writer 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
- import { type BuildingBlock, type BuildingBlockConfig, type GenerateBuildingBlockAggregationConfig } from './types.js';
2
+ import { type BuildingBlock, type BuildingBlockConfig } from './types.js';
3
3
  import { type CodeSnippet } from '../prompts/types.js';
4
+ export { generateBuildingBlockAggregation, getUI5XmlDocument } from './processAggregation.js';
4
5
  /**
5
6
  * Generates a building block into the provided xml view file.
6
7
  *
@@ -10,15 +11,6 @@ import { type CodeSnippet } from '../prompts/types.js';
10
11
  * @returns {Editor} the updated memfs editor instance
11
12
  */
12
13
  export declare function generateBuildingBlock<T extends BuildingBlock>(basePath: string, config: BuildingBlockConfig<T>, fs?: Editor): Promise<Editor>;
13
- /**
14
- * Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
15
- *
16
- * @param {string} basePath - the base path of the application
17
- * @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName and mContent
18
- * @param {Editor} [fs] - the memfs editor instance
19
- * @returns {Editor} the updated memfs editor instance
20
- */
21
- export declare function generateBuildingBlockAggregation(basePath: string, config: GenerateBuildingBlockAggregationConfig, fs?: Editor): Promise<Editor>;
22
14
  /**
23
15
  * Method returns the manifest content for the required dependency library.
24
16
  *
@@ -6,22 +6,23 @@ import { join, parse, relative } from 'node:path';
6
6
  import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
7
7
  import format from 'xml-formatter';
8
8
  import * as xpath from 'xpath';
9
- import { getMinimumUI5Version, getAppProgrammingLanguage } from '@sap-ux/project-access';
10
- import { BuildingBlockType, PAGE_AGGREGATIONS, PAGE_TEMPLATE_TYPE_FULL, bindingContextAbsolute } from './types.js';
9
+ import { getMinimumUI5Version } from '@sap-ux/project-access';
10
+ import { BuildingBlockType, PageTemplateType, bindingContextAbsolute } from './types.js';
11
11
  import { getErrorMessage, validateBasePath, validateDependenciesLibs } from '../common/validate.js';
12
12
  import { getTemplatePath } from '../templates.js';
13
13
  import { CodeSnippetLanguage } from '../prompts/types.js';
14
- import { CONFIG, copyTpl, createIdGenerator, detectTabSpacing, extendJSON, getRelativeTemplateComponentPath } from '../common/file.js';
14
+ import { CONFIG, createIdGenerator, detectTabSpacing, extendJSON, getRelativeTemplateComponentPath } from '../common/file.js';
15
15
  import { getManifest, getManifestPath } from '../common/utils.js';
16
16
  import { getOrAddNamespace } from './prompts/utils/xml.js';
17
17
  import { i18nNamespaces, translate } from '../i18n.js';
18
- import { processBuildingBlock } from './processor.js';
18
+ import { processBuildingBlock, resolveAggregationPath } from './processor.js';
19
+ import { appendPageAggregations, ensureMissingAggregation, getPageAggregationNames, getUI5XmlDocument, validateFullPageTemplateVersion } from './processAggregation.js';
20
+ export { generateBuildingBlockAggregation, getUI5XmlDocument } from './processAggregation.js';
19
21
  const PLACEHOLDERS = {
20
22
  'id': 'REPLACE_WITH_BUILDING_BLOCK_ID',
21
23
  'entitySet': 'REPLACE_WITH_ENTITY',
22
24
  'qualifier': 'REPLACE_WITH_A_QUALIFIER'
23
25
  };
24
- const PAGE_TEMPLATE_COMMENT = 'This is a sample template, event handlers should be added for implementation';
25
26
  /**
26
27
  * Returns true if the building block data represents a Page building block with the full template type.
27
28
  *
@@ -29,7 +30,7 @@ const PAGE_TEMPLATE_COMMENT = 'This is a sample template, event handlers should
29
30
  * @returns true if full Page template
30
31
  */
31
32
  function isFullPageTemplate(data) {
32
- return data.buildingBlockType === BuildingBlockType.Page && data.templateType === PAGE_TEMPLATE_TYPE_FULL;
33
+ return data.buildingBlockType === BuildingBlockType.Page && data.templateType === PageTemplateType.Full;
33
34
  }
34
35
  /**
35
36
  * Generates a building block into the provided xml view file.
@@ -51,6 +52,9 @@ export async function generateBuildingBlock(basePath, config, fs) {
51
52
  const { path: manifestPath, content: manifest } = await getManifest(basePath, fs);
52
53
  // Read the view xml and template files and update contents of the view xml file
53
54
  const xmlDocument = getUI5XmlDocument(basePath, viewOrFragmentPath, fs);
55
+ if (aggregationPath) {
56
+ ensureMissingAggregation(xmlDocument, aggregationPath);
57
+ }
54
58
  const { updatedAggregationPath, processedBuildingBlockData, hasAggregation, aggregationNamespace } = processBuildingBlock({ ...buildingBlockData, generateId: fnGenerateId }, xmlDocument, manifestPath, manifest, aggregationPath, fs);
55
59
  const templateConfig = {
56
60
  hasAggregation,
@@ -58,9 +62,12 @@ export async function generateBuildingBlock(basePath, config, fs) {
58
62
  };
59
63
  const templateDocument = getTemplateDocument({ ...processedBuildingBlockData, generateId: fnGenerateId }, xmlDocument, fs, manifest, templateConfig);
60
64
  const fullPageTemplate = isFullPageTemplate(buildingBlockData);
65
+ const pageAggregationNames = getPageAggregationNames(buildingBlockData);
61
66
  if (fullPageTemplate) {
62
- const pageData = buildingBlockData;
63
- appendPageAggregations(fs, xmlDocument, templateDocument, fnGenerateId, pageData);
67
+ validateFullPageTemplateVersion(manifest);
68
+ }
69
+ if (pageAggregationNames) {
70
+ appendPageAggregations(fs, xmlDocument, templateDocument, fnGenerateId, pageAggregationNames, fullPageTemplate);
64
71
  }
65
72
  if (buildingBlockData.buildingBlockType === BuildingBlockType.RichTextEditor ||
66
73
  buildingBlockData.buildingBlockType === BuildingBlockType.RichTextEditorButtonGroups) {
@@ -72,9 +79,6 @@ export async function generateBuildingBlock(basePath, config, fs) {
72
79
  getOrAddNamespace(xmlDocument, 'sap.fe.macros.richtexteditor', 'richtexteditor');
73
80
  }
74
81
  fs = updateViewFile(basePath, viewOrFragmentPath, updatedAggregationPath, xmlDocument, templateDocument, fs, config.replace);
75
- if (fullPageTemplate) {
76
- await applyPageControllerTemplate(fs, basePath, viewOrFragmentPath);
77
- }
78
82
  if (allowAutoAddDependencyLib && manifest && !validateDependenciesLibs(manifest, ['sap.fe.macros'])) {
79
83
  // "sap.fe.macros" is missing - enhance manifest.json for missing "sap.fe.macros"
80
84
  const manifestPath = await getManifestPath(basePath, fs);
@@ -89,271 +93,6 @@ export async function generateBuildingBlock(basePath, config, fs) {
89
93
  }
90
94
  return fs;
91
95
  }
92
- /**
93
- * Resolves the sap.fe.macros namespace prefix from the view document.
94
- * If sap.fe.macros is the default namespace (no prefix), declares xmlns:macros on the document element
95
- * so that generated prefixed elements like <macros:items> remain valid.
96
- *
97
- * @param xmlDocument - the view XML document
98
- * @returns the resolved namespace prefix string (e.g. 'macros')
99
- */
100
- function resolveMacrosPrefix(xmlDocument) {
101
- const prefix = getOrAddNamespace(xmlDocument, 'sap.fe.macros', 'macros');
102
- if (prefix === '') {
103
- xmlDocument.documentElement.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:macros', 'sap.fe.macros');
104
- return 'macros';
105
- }
106
- return prefix;
107
- }
108
- /**
109
- * Renders a Page aggregation EJS template and parses it as an XML fragment document.
110
- * Inherits all xmlns:* declarations from the view root so inner content can use any view-declared prefix.
111
- *
112
- * @param fs - the memfs editor instance
113
- * @param aggName - the aggregation name (e.g. 'footer', 'items')
114
- * @param aggContext - the EJS template context
115
- * @param aggContext.macrosPrefix - the namespace prefix string (e.g. 'macros:')
116
- * @param aggContext.mContent - optional inner XML content for the aggregation
117
- * @param aggContext.aggId - the generated unique ID for the aggregation element
118
- * @param fragMacrosNS - the namespace prefix resolved for sap.fe.macros
119
- * @param xmlDocument - the view XML document (used to inherit namespace declarations)
120
- * @returns parsed XML document whose documentElement contains the aggregation child nodes
121
- */
122
- function buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument) {
123
- const aggPath = getTemplatePath(`/building-block/page/${aggName}.xml`);
124
- const aggContent = render(fs.read(aggPath), aggContext, {}); // NOSONAR - template is a controlled file on disk, not user input
125
- const extraNamespaces = Array.from(xmlDocument.documentElement.attributes)
126
- .filter((a) => a.name.startsWith('xmlns:') && a.name !== `xmlns:${fragMacrosNS}` && a.name !== 'xmlns:m')
127
- .map((a) => `${a.name}="${a.value}"`)
128
- .join(' ');
129
- const wrapped = `<root xmlns:${fragMacrosNS}="sap.fe.macros" xmlns="sap.m" xmlns:m="sap.m" ${extraNamespaces}>${aggContent}</root>`;
130
- const errorHandler = (level, message) => {
131
- throw new Error(`Unable to parse page aggregation fragment '${aggName}'. Details: [${level}] - ${message}`);
132
- };
133
- return new DOMParser({ errorHandler }).parseFromString(wrapped, 'text/xml');
134
- }
135
- /**
136
- * Appends the 7 Page building block aggregation fragments as child elements of the templateDocument root.
137
- *
138
- * @param {Editor} fs - the memfs editor instance
139
- * @param {Document} xmlDocument - the view XML document (used to resolve namespace prefixes)
140
- * @param {Document} templateDocument - the template document whose root element receives the children
141
- * @param {IdGeneratorFunction} generateId - function to generate unique IDs
142
- * @param {Page} pageData - the Page building block data containing optional aggregation mContent
143
- */
144
- function appendPageAggregations(fs, xmlDocument, templateDocument, generateId, pageData) {
145
- const fragMacrosNS = resolveMacrosPrefix(xmlDocument);
146
- const macrosPrefix = `${fragMacrosNS}:`;
147
- const pageElement = templateDocument.documentElement;
148
- pageElement.appendChild(templateDocument.createComment(PAGE_TEMPLATE_COMMENT));
149
- for (const aggName of PAGE_AGGREGATIONS) {
150
- const mContent = pageData.aggregations?.[aggName] ?? '';
151
- const aggId = generateId(aggName);
152
- const aggContext = { macrosPrefix, mContent, aggId };
153
- const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
154
- for (const node of Array.from(aggDoc.documentElement.childNodes)) {
155
- if (node.nodeType === 1 /* Element */) {
156
- node.setAttribute('id', aggId);
157
- pageElement.appendChild(templateDocument.importNode(node, true));
158
- }
159
- }
160
- }
161
- }
162
- /**
163
- * Returns the local name of an Element if it belongs to the sap.fe.macros namespace, otherwise an empty string.
164
- * This ensures only Page aggregation elements are sorted by position; non-macros elements fall back to the items slot.
165
- *
166
- * @param el - the DOM Element
167
- * @returns the local name string, or '' if not a sap.fe.macros element
168
- */
169
- function getElementLocalName(el) {
170
- if (el.namespaceURI !== 'sap.fe.macros') {
171
- return '';
172
- }
173
- return typeof el.localName === 'string' ? el.localName : '';
174
- }
175
- /**
176
- * Builds a comparator for sorting XmlAggregationGroups by their position in aggNames.
177
- * Unknown elements fall back to the position of 'items'. Ties are broken by original index.
178
- *
179
- * @param aggNames - ordered list of aggregation names
180
- * @returns comparator function for Array.prototype.sort
181
- */
182
- function buildAggregationComparator(aggNames) {
183
- const itemsIdx = aggNames.indexOf('items');
184
- const fallbackIdx = itemsIdx === -1 ? aggNames.length : itemsIdx;
185
- return (a, b) => {
186
- const aIdx = aggNames.indexOf(getElementLocalName(a.element));
187
- const bIdx = aggNames.indexOf(getElementLocalName(b.element));
188
- const aOrder = aIdx === -1 ? fallbackIdx : aIdx;
189
- const bOrder = bIdx === -1 ? fallbackIdx : bIdx;
190
- return aOrder === bOrder ? a.originalIndex - b.originalIndex : aOrder - bOrder;
191
- };
192
- }
193
- /**
194
- * Reorders the child elements of a macros:Page node to match the canonical PAGE_AGGREGATIONS order.
195
- * Preserves relative order of siblings with the same local name. Pure whitespace text nodes are dropped
196
- * because the xml-formatter call that follows will regenerate proper indentation.
197
- *
198
- * @param pageElement - the macros:Page DOM node whose children should be sorted
199
- */
200
- function sortPageAggregationChildren(pageElement) {
201
- const allChildren = Array.from(pageElement.childNodes);
202
- const aggNames = PAGE_AGGREGATIONS;
203
- // Build pairs of [preceding comments, element] to preserve user comments.
204
- // Comments that appear before the first element are treated as leading and will remain before all aggregation elements.
205
- const groups = [];
206
- const leadingComments = [];
207
- let pendingComments = [];
208
- let firstElementSeen = false;
209
- for (const node of allChildren) {
210
- if (node.nodeType === 8 /* Comment */) {
211
- // Comments before the first element are leading; after, they are pending
212
- (firstElementSeen ? pendingComments : leadingComments).push(node);
213
- }
214
- else if (node.nodeType === 1 /* Element */) {
215
- firstElementSeen = true;
216
- groups.push({ comments: pendingComments, element: node, originalIndex: groups.length });
217
- pendingComments = [];
218
- }
219
- else if (node.nodeType === 3 /* Text */ && node.data?.trim()) {
220
- // Preserve non-whitespace text nodes with their surrounding group
221
- pendingComments.push(node);
222
- }
223
- // Pure whitespace text nodes are intentionally dropped (xml-formatter regenerates indentation)
224
- }
225
- groups.sort(buildAggregationComparator(aggNames));
226
- while (pageElement.firstChild) {
227
- pageElement.removeChild(pageElement.firstChild); // NOSONAR - xmldom nodes do not implement Node.remove()
228
- }
229
- // Re-insert leading comments first (always before any element)
230
- for (const comment of leadingComments) {
231
- pageElement.appendChild(comment);
232
- }
233
- for (const { comments, element } of groups) {
234
- for (const comment of comments) {
235
- pageElement.appendChild(comment);
236
- }
237
- pageElement.appendChild(element);
238
- }
239
- // Trailing orphan comments (after the last element)
240
- for (const comment of pendingComments) {
241
- pageElement.appendChild(comment);
242
- }
243
- }
244
- /**
245
- * Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
246
- *
247
- * @param {string} basePath - the base path of the application
248
- * @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName and mContent
249
- * @param {Editor} [fs] - the memfs editor instance
250
- * @returns {Editor} the updated memfs editor instance
251
- */
252
- export async function generateBuildingBlockAggregation(basePath, config, fs) {
253
- const { viewPath, buildingBlockType, aggregationName: aggName, mContent = '' } = config;
254
- fs ??= create(createStorage());
255
- if (buildingBlockType !== BuildingBlockType.Page) {
256
- throw new Error(`generateBuildingBlockAggregation: unsupported building block type '${buildingBlockType}'. Only 'Page' is currently supported.`);
257
- }
258
- const xmlDocument = getUI5XmlDocument(basePath, viewPath, fs);
259
- const generateId = await createIdGenerator({ basePath, fsEditor: fs });
260
- const aggId = generateId(aggName);
261
- const fragMacrosNS = resolveMacrosPrefix(xmlDocument);
262
- const macrosPrefix = `${fragMacrosNS}:`;
263
- const aggContext = { macrosPrefix, mContent, aggId };
264
- const aggDoc = buildPageAggregationFragment(fs, aggName, aggContext, fragMacrosNS, xmlDocument);
265
- const nsMap = xmlDocument.documentElement?._nsMap ?? {};
266
- // Prefix-agnostic XPath — works regardless of the alias used in the view for sap.fe.macros.
267
- const xpathSelect = xpath.useNamespaces(nsMap);
268
- const pageNodes = xpathSelect(`//*[local-name()='Page' and namespace-uri()='sap.fe.macros']`, xmlDocument);
269
- if (!pageNodes || !Array.isArray(pageNodes) || pageNodes.length === 0) {
270
- throw new Error(`Page element (sap.fe.macros) not found in view ${viewPath}.`);
271
- }
272
- const pageElement = pageNodes[0];
273
- if (aggName === 'footer' && pageElement.nodeType === 1 /* Element */) {
274
- pageElement.setAttribute('showFooter', 'true');
275
- }
276
- const childNodes = Array.from(pageElement.childNodes);
277
- const hasExistingAggregation = childNodes.some((node) => node.nodeType === 1 /* Element */ &&
278
- node.localName === aggName &&
279
- node.namespaceURI === 'sap.fe.macros');
280
- if (hasExistingAggregation) {
281
- sortPageAggregationChildren(pageElement);
282
- const existingXmlContent = new XMLSerializer().serializeToString(xmlDocument);
283
- fs.write(join(basePath, viewPath), format(existingXmlContent));
284
- return fs;
285
- }
286
- const hasExistingElementChildren = childNodes.some((n) => n.nodeType === 1 /* Element */);
287
- const hasTemplateComment = childNodes.some((n) => n.nodeType === 8 /* Comment */ && n.data?.includes(PAGE_TEMPLATE_COMMENT));
288
- if (!hasExistingElementChildren && !hasTemplateComment) {
289
- pageElement.appendChild(xmlDocument.createComment(PAGE_TEMPLATE_COMMENT));
290
- }
291
- for (const node of Array.from(aggDoc.documentElement.childNodes)) {
292
- if (node.nodeType === 1 /* Element */) {
293
- node.setAttribute('id', aggId);
294
- pageElement.appendChild(xmlDocument.importNode(node, true));
295
- }
296
- }
297
- sortPageAggregationChildren(pageElement);
298
- const newXmlContent = new XMLSerializer().serializeToString(xmlDocument);
299
- fs.write(join(basePath, viewPath), format(newXmlContent));
300
- return fs;
301
- }
302
- /**
303
- * Copies the Page controller template (JS or TS) into the view directory if no controller file exists yet.
304
- * Uses getAppProgrammingLanguage to decide whether to generate a JS or TS controller stub.
305
- *
306
- * @param {Editor} fs - the memfs editor instance
307
- * @param {string} basePath - the base path of the application
308
- * @param {string} viewOrFragmentPath - the relative path of the view/fragment file
309
- */
310
- async function applyPageControllerTemplate(fs, basePath, viewOrFragmentPath) {
311
- if (!viewOrFragmentPath.endsWith('.view.xml')) {
312
- return;
313
- }
314
- const { dir: viewDir, name: viewName } = parse(viewOrFragmentPath);
315
- const viewBaseName = viewName.replace(/\.view$/, '');
316
- const tsControllerPath = join(basePath, viewDir, `${viewBaseName}.controller.ts`);
317
- const jsControllerPath = join(basePath, viewDir, `${viewBaseName}.controller.js`);
318
- // Skip if a controller already exists in either language to avoid duplicate stubs
319
- if (fs.exists(tsControllerPath) || fs.exists(jsControllerPath)) {
320
- return;
321
- }
322
- const detectedLanguage = await getAppProgrammingLanguage(basePath, fs);
323
- const isTypeScript = detectedLanguage === 'TypeScript';
324
- const controllerExt = isTypeScript ? 'ts' : 'js';
325
- const controllerPath = isTypeScript ? tsControllerPath : jsControllerPath;
326
- copyTpl(fs, getTemplatePath(`/building-block/page/Controller.${controllerExt}`), controllerPath);
327
- }
328
- /**
329
- * Returns the UI5 xml file document (view/fragment).
330
- *
331
- * @param {string} basePath - the base path
332
- * @param {string} viewPath - the path of the xml view relative to the base path
333
- * @param {Editor} fs - the memfs editor instance
334
- * @returns {Document} the view xml file document
335
- */
336
- function getUI5XmlDocument(basePath, viewPath, fs) {
337
- let viewContent;
338
- try {
339
- viewContent = fs.read(join(basePath, viewPath));
340
- }
341
- catch (error) {
342
- throw new Error(`Unable to read xml view file. Details: ${getErrorMessage(error)}`);
343
- }
344
- const errorHandler = (level, message) => {
345
- throw new Error(`Unable to parse xml view file. Details: [${level}] - ${message}`);
346
- };
347
- // Parse the xml view content
348
- let viewDocument;
349
- try {
350
- viewDocument = new DOMParser({ errorHandler }).parseFromString(viewContent, 'text/xml');
351
- }
352
- catch (error) {
353
- throw new Error(`Unable to parse xml view file. Details: ${getErrorMessage(error)}`);
354
- }
355
- return viewDocument;
356
- }
357
96
  /**
358
97
  * Method returns default values for metadata path.
359
98
  *
@@ -513,14 +252,17 @@ function getTemplateDocument(buildingBlockData, viewDocument, fs, manifest, temp
513
252
  * @returns {Editor} the updated memfs editor instance
514
253
  */
515
254
  function updateViewFile(basePath, viewPath, aggregationPath, viewDocument, templateDocument, fs, replace = false) {
516
- const firstChild = viewDocument.firstChild;
517
- if (!firstChild) {
255
+ const root = viewDocument.documentElement ?? viewDocument.firstChild;
256
+ if (!root) {
518
257
  throw new Error(`Unable to read namespace map from view ${viewPath}.`);
519
258
  }
520
- const nsMap = firstChild?._nsMap ?? {};
259
+ const nsMap = root?._nsMap ?? {};
521
260
  const xpathSelect = xpath.useNamespaces(nsMap);
261
+ // XPath 1.0 does not support default namespaces: unprefixed names match only no-namespace
262
+ // elements. Rewrite each unprefixed step to a *[local-name()='X'] predicate.
263
+ const resolvedPath = resolveAggregationPath(aggregationPath);
522
264
  // Find target aggregated element and append template as child
523
- const targetNodes = xpathSelect(aggregationPath, viewDocument);
265
+ const targetNodes = xpathSelect(resolvedPath, viewDocument);
524
266
  if (targetNodes && Array.isArray(targetNodes) && targetNodes.length > 0) {
525
267
  const targetNode = targetNodes[0];
526
268
  const sourceNode = viewDocument.importNode(templateDocument.documentElement, true);
@@ -577,11 +319,13 @@ export async function getSerializedFileContent(basePath, config, fs) {
577
319
  const { content: manifest, path: manifestPath } = await getManifest(basePath, fs, false);
578
320
  const fnGenerateId = buildingBlockData.generateId ?? (await createIdGenerator({ basePath, fsEditor: fs }));
579
321
  const content = getTemplateContent({ ...buildingBlockData, generateId: fnGenerateId }, xmlDocument, manifest, fs, true);
580
- // For the full Page template, augment the snippet with all 7 aggregations
322
+ // For full Page templates, augment the snippet with all PAGE_AGGREGATIONS
581
323
  let viewOrFragmentContent = content;
582
- const pageData = buildingBlockData;
583
- const isFullPage = isFullPageTemplate(buildingBlockData);
584
- if (isFullPage) {
324
+ const pageAggNames = getPageAggregationNames(buildingBlockData);
325
+ if (pageAggNames) {
326
+ if (isFullPageTemplate(buildingBlockData)) {
327
+ validateFullPageTemplateVersion(manifest);
328
+ }
585
329
  // Use the real view document for namespace resolution if available, otherwise create a minimal fallback
586
330
  const nsDoc = xmlDocument ??
587
331
  new DOMParser().parseFromString('<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns:macros="sap.fe.macros" xmlns="sap.m"/>', 'text/xml');
@@ -593,7 +337,7 @@ export async function getSerializedFileContent(basePath, config, fs) {
593
337
  const snippetMacrosNS = getOrAddNamespace(nsDoc, 'sap.fe.macros', 'macros') || 'macros';
594
338
  const snippetContent = `${content}`.replace(new RegExp(`^<(${snippetMacrosNS}:Page)`), `<$1 xmlns:${snippetMacrosNS}="sap.fe.macros"`);
595
339
  const snippetDoc = new DOMParser({ errorHandler: snippetErrorHandler }).parseFromString(snippetContent, 'text/xml');
596
- appendPageAggregations(fs, nsDoc, snippetDoc, fnGenerateId, pageData);
340
+ appendPageAggregations(fs, nsDoc, snippetDoc, fnGenerateId, pageAggNames, isFullPageTemplate(buildingBlockData));
597
341
  const resultNode = snippetDoc.documentElement;
598
342
  viewOrFragmentContent = resultNode ? format(new XMLSerializer().serializeToString(resultNode)) : content;
599
343
  }
@@ -0,0 +1,81 @@
1
+ import type { Editor } from 'mem-fs-editor';
2
+ import { type BuildingBlock, type PageAggregationName, type GenerateBuildingBlockAggregationConfig } from './types.js';
3
+ import type { Manifest } from '../common/types.js';
4
+ import { type IdGeneratorFunction } from '../common/file.js';
5
+ /**
6
+ * Returns the UI5 xml file document (view/fragment).
7
+ *
8
+ * @param {string} basePath - the base path
9
+ * @param {string} viewPath - the path of the xml view relative to the base path
10
+ * @param {Editor} fs - the memfs editor instance
11
+ * @returns {Document} the view xml file document
12
+ */
13
+ export declare function getUI5XmlDocument(basePath: string, viewPath: string, fs: Editor): Document;
14
+ /**
15
+ * Returns the aggregation names to append for a Page building block, or undefined if not a Page.
16
+ * Full template appends all PAGE_AGGREGATIONS; basic template appends nothing.
17
+ *
18
+ * @param data - the building block data
19
+ * @returns aggregation names array, or undefined if not a Page building block
20
+ */
21
+ export declare function getPageAggregationNames(data: BuildingBlock): readonly PageAggregationName[] | undefined;
22
+ /**
23
+ * Throws if the manifest's minUI5Version is set and does not meet the 1.145.0 requirement for the full Page template.
24
+ * If minUI5Version is not set, it is treated as latest and passes the check.
25
+ *
26
+ * @param manifest - the manifest content, or undefined if not available
27
+ */
28
+ export declare function validateFullPageTemplateVersion(manifest: Manifest | undefined): void;
29
+ /** IDs needed per aggregation template, keyed by the variable name used in the EJS template. */
30
+ /** Controls to generate IDs for per aggregation. `key` is the EJS template variable name, `base` is passed to generateId. */
31
+ export declare const AGGREGATION_ID_KEYS: Partial<Record<PageAggregationName, {
32
+ key: string;
33
+ base: string;
34
+ }[]>>;
35
+ /**
36
+ * Generates a map of unique IDs for all controls in a given Page aggregation template.
37
+ * Keys match the `ids.*` variable names referenced in the EJS templates.
38
+ *
39
+ * @param aggName - the aggregation name
40
+ * @param generateId - the project-aware ID generator
41
+ * @returns an object mapping each template variable name to a unique ID string
42
+ */
43
+ export declare function buildAggregationIds(aggName: PageAggregationName, generateId: IdGeneratorFunction): Record<string, string>;
44
+ /**
45
+ *
46
+ * @param {Editor} fs - the memfs editor instance
47
+ * @param {Document} xmlDocument - the view XML document (used to resolve namespace prefixes)
48
+ * @param {Document} templateDocument - the template document whose root element receives the children
49
+ * @param {IdGeneratorFunction} generateId - function to generate unique IDs
50
+ * @param {readonly PageAggregationName[]} [aggNames] - aggregation names to append; defaults to all PAGE_AGGREGATIONS
51
+ * @param useDefaults - when true, the items aggregation renders its default IconTabBar content
52
+ */
53
+ export declare function appendPageAggregations(fs: Editor, xmlDocument: Document, templateDocument: Document, generateId: IdGeneratorFunction, aggNames?: readonly PageAggregationName[], useDefaults?: boolean): void;
54
+ /**
55
+ * Reorders the child elements of a macros:Page node to match the canonical PAGE_AGGREGATIONS order.
56
+ * Preserves relative order of siblings with the same local name. Pure whitespace text nodes are dropped
57
+ * because the xml-formatter call that follows will regenerate proper indentation.
58
+ *
59
+ * @param pageElement - the macros:Page DOM node whose children should be sorted
60
+ */
61
+ export declare function sortPageAggregationChildren(pageElement: Node): void;
62
+ /**
63
+ * Ensures a namespaced aggregation element exists in the XML document at the given aggregation path.
64
+ * If the final segment (e.g. 'macros:items') is missing from the DOM, the parent node is located
65
+ * via the path prefix and the element is inserted in place. This allows generateBuildingBlock to
66
+ * handle missing aggregation containers in a single write pass, avoiding a separate commit.
67
+ *
68
+ * @param {Document} xmlDocument - The XML document to mutate
69
+ * @param {string} aggregationPath - Full XPath to the target aggregation (e.g. '/mvc:View/macros:Page/macros:items')
70
+ */
71
+ export declare function ensureMissingAggregation(xmlDocument: Document, aggregationPath: string): void;
72
+ /**
73
+ * Appends a single Page building block aggregation template to an existing `<macros:Page>` element in a view XML file.
74
+ *
75
+ * @param {string} basePath - the base path of the application
76
+ * @param {GenerateBuildingBlockAggregationConfig} config - the aggregation configuration containing aggregationName
77
+ * @param {Editor} [fs] - the memfs editor instance
78
+ * @returns {Editor} the updated memfs editor instance
79
+ */
80
+ export declare function generateBuildingBlockAggregation(basePath: string, config: GenerateBuildingBlockAggregationConfig, fs?: Editor): Promise<Editor>;
81
+ //# sourceMappingURL=processAggregation.d.ts.map