@ui5/task-adaptation 1.4.2 → 1.5.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +16 -12
  3. package/REUSE.toml +10 -0
  4. package/dist/annotationManager.d.ts +1 -1
  5. package/dist/annotationManager.js +5 -8
  6. package/dist/annotations/dataSource/dataSource.js +0 -1
  7. package/dist/annotations/serviceRequestor.js +3 -15
  8. package/dist/appVariantManager.d.ts +16 -11
  9. package/dist/appVariantManager.js +56 -83
  10. package/dist/baseAppManager.d.ts +16 -16
  11. package/dist/baseAppManager.js +58 -81
  12. package/dist/bundle.d.ts +2 -3
  13. package/dist/bundle.js +3834 -3490
  14. package/dist/cache/cacheHolder.d.ts +18 -0
  15. package/dist/cache/cacheHolder.js +80 -0
  16. package/dist/i18nManager.js +11 -9
  17. package/dist/index.js +34 -9
  18. package/dist/model/appVariantIdHierarchyItem.d.ts +5 -0
  19. package/dist/model/appVariantIdHierarchyItem.js +2 -0
  20. package/dist/model/configuration.d.ts +2 -2
  21. package/dist/model/language.d.ts +3 -3
  22. package/dist/model/language.js +11 -4
  23. package/dist/model/types.d.ts +3 -1
  24. package/dist/processors/abapProcessor.d.ts +5 -5
  25. package/dist/processors/abapProcessor.js +19 -7
  26. package/dist/processors/cfProcessor.d.ts +4 -4
  27. package/dist/processors/cfProcessor.js +29 -13
  28. package/dist/processors/processor.d.ts +4 -2
  29. package/dist/processors/processor.js +3 -5
  30. package/dist/repositories/abapRepoManager.d.ts +3 -1
  31. package/dist/repositories/abapRepoManager.js +24 -5
  32. package/dist/repositories/html5RepoManager.js +3 -2
  33. package/dist/util/cfUtil.d.ts +1 -0
  34. package/dist/util/cfUtil.js +27 -23
  35. package/dist/util/commonUtil.d.ts +5 -3
  36. package/dist/util/commonUtil.js +104 -31
  37. package/dist/util/filesUtil.d.ts +17 -0
  38. package/dist/util/filesUtil.js +49 -0
  39. package/dist/util/i18nMerger.d.ts +15 -20
  40. package/dist/util/i18nMerger.js +46 -64
  41. package/dist/util/renamingHandlers/manifestHandler.d.ts +6 -0
  42. package/dist/util/renamingHandlers/manifestHandler.js +20 -0
  43. package/dist/util/renamingHandlers/renamingHandler.d.ts +4 -0
  44. package/dist/util/renamingHandlers/renamingHandler.js +2 -0
  45. package/dist/util/requestUtil.d.ts +2 -1
  46. package/dist/util/resourceUtil.d.ts +3 -1
  47. package/dist/util/resourceUtil.js +15 -2
  48. package/eslint.config.js +20 -4
  49. package/package.json +28 -22
  50. package/scripts/rollup/bundle.d.ts +2 -3
  51. package/scripts/rollup/bundleDefinition.js +2 -2
  52. package/scripts/rollup/overrides/sap/ui/fl/Utils.js +13 -0
  53. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/changes/FlexCustomData.js +13 -0
  54. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/flexObjects/AnnotationChange.js +11 -0
  55. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/flexObjects/AppDescriptorChange.js +68 -0
  56. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/flexObjects/VariantChange.js +11 -0
  57. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/flexObjects/VariantManagementChange.js +11 -0
  58. package/scripts/rollup/overrides/sap/ui/fl/apply/_internal/flexState/controlVariants/VariantManagementState.js +13 -0
  59. package/scripts/rollup/overrides/sap/ui/fl/initial/_internal/changeHandlers/ChangeHandlerRegistration.js +13 -0
  60. package/scripts/rollup/overrides/sap/ui/fl/initial/_internal/changeHandlers/ChangeHandlerStorage.js +14 -0
  61. package/scripts/rollup/project/ui5.yaml +1 -1
  62. package/scripts/test-integration-prep.sh +4 -0
  63. package/types/ui5.d.ts +10 -0
  64. package/dist/cache/annotationsCacheManager.d.ts +0 -8
  65. package/dist/cache/annotationsCacheManager.js +0 -16
  66. package/dist/cache/baseAppFilesCacheManager.d.ts +0 -6
  67. package/dist/cache/baseAppFilesCacheManager.js +0 -12
  68. package/dist/cache/cacheManager.d.ts +0 -16
  69. package/dist/cache/cacheManager.js +0 -65
  70. package/scripts/rollup/overrides/sap/ui/fl/Change.js +0 -74
@@ -1,4 +1,5 @@
1
- import { cfCreateService, cfGetInstanceCredentials, cfGetTarget } from "@sap/cf-tools/out/src/cf-local.js";
1
+ import { cfCreateService, cfGetInstanceCredentials } from "@sap/cf-tools/out/src/cf-local.js";
2
+ import { getSpaceGuidThrowIfUndefined } from "@sap/cf-tools/out/src/utils.js";
2
3
  import { Cli } from "@sap/cf-tools/out/src/cli.js";
3
4
  import { eFilters } from "@sap/cf-tools/out/src/types.js";
4
5
  import { getLogger } from "@ui5/logger";
@@ -34,16 +35,20 @@ export default class CFUtil {
34
35
  }
35
36
  static async createService(params) {
36
37
  log.verbose(`Creating a service instance with parameters: ${JSON.stringify(params)}`);
37
- const resources = await this.requestCfApi(`/v3/service_plans?names=${params.planName}&space_guids=${params.spaceGuid}`);
38
- const publicPlan = resources.find(resource => resource.visibility_type === "public");
39
- if (!publicPlan) {
40
- throw new Error(`Cannot find a public plan by name '${params.serviceName}' in space '${params.spaceGuid}'`);
38
+ const serviceOfferings = await this.requestCfApi(`/v3/service_offerings?names=${params.serviceName}`);
39
+ if (serviceOfferings.length === 0) {
40
+ throw new Error(`Cannot find a service offering by name '${params.serviceName}'`);
41
+ }
42
+ const plans = await this.requestCfApi(`/v3/service_plans?service_offering_guids=${serviceOfferings[0].guid}`);
43
+ const plan = plans.find(plan => plan.name === params.planName);
44
+ if (!plan) {
45
+ throw new Error(`Cannot find a plan by name '${params.planName}' for service '${params.serviceName}'`);
41
46
  }
42
47
  try {
43
- await cfCreateService(publicPlan.guid, params.serviceName, params.parameters, params.tags);
48
+ await cfCreateService(plan.guid, params.serviceInstanceName, params.parameters, params.tags);
44
49
  }
45
50
  catch (error) {
46
- throw new Error(`Cannot create a service instance '${params.serviceName}' in space '${params.spaceGuid}': ${error.message}`);
51
+ throw new Error(`Cannot create a service instance '${params.serviceInstanceName}' in space '${params.spaceGuid}': ${error.message}`);
47
52
  }
48
53
  }
49
54
  static async getOrCreateServiceKeys(serviceInstance) {
@@ -73,7 +78,7 @@ export default class CFUtil {
73
78
  return this.cfExecute(["create-service-key", serviceInstanceName, serviceKeyName]);
74
79
  }
75
80
  catch (error) {
76
- throw new Error(`Couldn't create a service key for instance: ${serviceInstanceName}`);
81
+ throw new Error(`Couldn't create a service key for instance: ${serviceInstanceName}: ${error}`);
77
82
  }
78
83
  }
79
84
  static async getServiceInstance(params) {
@@ -92,9 +97,19 @@ export default class CFUtil {
92
97
  guid: service.guid
93
98
  }));
94
99
  }
100
+ static processErrors(json) {
101
+ if (json?.errors?.length > 0) {
102
+ const message = JSON.stringify(json.errors);
103
+ if (json?.errors?.some((e) => e.title === "CF-NotAuthenticated" || e.code === 10002)) {
104
+ throw new Error(`Authentication error. Use 'cf login' to authenticate in Cloud Foundry: ${message}`);
105
+ }
106
+ throw new Error(`Failed sending request to Cloud Foundry: ${message}`);
107
+ }
108
+ }
95
109
  static async requestCfApi(url) {
96
110
  const response = await this.cfExecute(["curl", url]);
97
111
  const json = this.parseJson(response);
112
+ this.processErrors(json);
98
113
  const resources = json?.resources;
99
114
  const totalPages = json?.pagination?.total_pages;
100
115
  if (totalPages > 1) {
@@ -105,7 +120,7 @@ export default class CFUtil {
105
120
  return this.parseJson(response)?.resources || [];
106
121
  })).then(resources => [].concat(...resources)));
107
122
  }
108
- return resources;
123
+ return resources ?? [];
109
124
  }
110
125
  static getOAuthToken() {
111
126
  return this.cfExecute(["oauth-token"]);
@@ -152,20 +167,9 @@ export default class CFUtil {
152
167
  * @memberof CFUtil
153
168
  */
154
169
  static async getSpaceGuid(spaceGuid) {
155
- if (spaceGuid == null) {
156
- const spaceName = (await cfGetTarget())?.space;
157
- if (spaceName) {
158
- const resources = await this.requestCfApi(`/v3/spaces?names=${spaceName}`);
159
- for (const resource of resources) {
160
- spaceGuid = resource.guid;
161
- break;
162
- }
163
- }
164
- }
165
- if (spaceGuid == null) {
166
- throw new Error("Please login to Cloud Foundry with 'cf login' and try again");
167
- }
168
- return spaceGuid;
170
+ return spaceGuid ?? getSpaceGuidThrowIfUndefined().catch((e) => {
171
+ throw new Error("Please specify space and org guids in ui5.yaml or login to Cloud Foundry with 'cf login' and try again: " + e.message);
172
+ });
169
173
  }
170
174
  }
171
175
  //# sourceMappingURL=cfUtil.js.map
@@ -1,13 +1,15 @@
1
1
  import { IConfiguration } from "../model/types.js";
2
2
  import Language from "../model/language.js";
3
3
  export declare function dotToUnderscore(value: string): string;
4
- export declare function validateObject<T extends Object>(options: T, properties: Array<keyof T>, message: string): void;
4
+ export declare function validateObject<T extends object>(options: T, properties: Array<keyof T>, message: string): void;
5
5
  export declare function escapeRegex(update: string): string;
6
- export declare function renameResources(files: Map<string, string>, search: string, replacement: string): Map<string, string>;
6
+ export declare function renameResources(files: ReadonlyMap<string, string>, search: string[], replacement: string, ignoreInStrings?: string[]): Map<string, string>;
7
+ export declare function rename(content: string, searchTerms: string[], replacement: string, ignoreInStrings?: string[]): string;
7
8
  export declare function insertInArray<T>(array: T[], index: number, insert: T): void;
8
9
  export declare function writeTempAnnotations({ writeTempFiles }: IConfiguration, name: string, language: Language, content: string): void;
9
- export declare function removePropertiesExtension(filePath: string): string;
10
+ export declare function trimExtension(filePath: string): string;
10
11
  export declare function traverse(json: any, paths: string[], callback: (json: any, key: string | number, paths: string[]) => void): void;
11
12
  export declare function logBuilderVersion(): void;
12
13
  export declare function logBetaUsage(): void;
13
14
  export declare function getUniqueName(existingNames: string[], template: string): string;
15
+ export declare function getI18nPropertyKeys(files: ReadonlyMap<string, string>): string[];
@@ -17,38 +17,96 @@ export function validateObject(options, properties, message) {
17
17
  export function escapeRegex(update) {
18
18
  return update.replaceAll(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
19
19
  }
20
- export function renameResources(files, search, replacement) {
21
- // The current regex works if the old Id is contained in the new Id, given
22
- // that they do not have the same beginning.
23
- // more complete alternative: /((?<!newIdStart)|(?!newIdEnd))oldId/g
24
- let escapedSearch;
25
- if (replacement.includes(search)) {
26
- const [before] = replacement.split(search);
27
- // Matches a position in the string that is not immediately preceded by
28
- // the string "before". Since we won't replace anyway, we should also
29
- // ignore one with the slashes.
30
- const escapedBefore = escapeRegex(before).replaceAll("\\.", "[\\./]");
31
- escapedSearch = `(?<!${escapedBefore})${escapeRegex(search)}`;
20
+ export function renameResources(files, search, replacement, ignoreInStrings = []) {
21
+ return new Map([...files].map(([filepath, content]) => [filepath, rename(content, search, replacement, ignoreInStrings)]));
22
+ }
23
+ export function rename(content, searchTerms, replacement, ignoreInStrings = []) {
24
+ if (replacement.includes(".") && !searchTerms.some(searchTerm => searchTerm.includes("."))) {
25
+ throw new Error("Ambiguous reference and appVariantId: both should contains dots or both should not contain dots.");
32
26
  }
33
- else {
34
- escapedSearch = escapeRegex(search);
27
+ if (!content || !searchTerms || searchTerms.length === 0) {
28
+ return content;
35
29
  }
36
- const dotToSlash = (update) => update.replaceAll(".", "\/");
37
- const replaces = [
38
- {
39
- regexp: new RegExp(escapedSearch, "g"),
40
- replacement
41
- },
42
- {
43
- regexp: new RegExp(dotToSlash(escapedSearch), "g"),
44
- replacement: dotToSlash(replacement)
30
+ const dotToSlash = (str) => str.replaceAll(".", "\/");
31
+ const replacementSlash = dotToSlash(replacement);
32
+ // We don't want to replace in adaptation project ids
33
+ ignoreInStrings.push(replacement);
34
+ ignoreInStrings.push(replacementSlash);
35
+ let start = 0;
36
+ while (true) {
37
+ // If we don't replace some strings in the content - we find all of them
38
+ // and then don't replace inside their start and end indices.
39
+ const ignoredStrings = ignoreInStrings.map(string => {
40
+ return findAllOccurrences(content, string, start).map(i => ({ start: i, end: i + string.length }));
41
+ }).filter(arr => arr.length > 0) || [];
42
+ // We find the next search index with dots and slashes. Then we replace
43
+ // the nearest one and start search again in the next loop step.
44
+ const indices = new Array();
45
+ for (const searchTerm of searchTerms) {
46
+ const searchTermSlash = dotToSlash(searchTerm);
47
+ indices.push({
48
+ i: content.indexOf(searchTerm, start),
49
+ replacement,
50
+ searchTerm
51
+ });
52
+ indices.push({
53
+ i: content.indexOf(searchTermSlash, start),
54
+ replacement: replacementSlash,
55
+ searchTerm: searchTermSlash
56
+ });
45
57
  }
46
- ];
47
- files.forEach((content, filepath, map) => {
48
- map.set(filepath, replaces.reduce((p, c) => p.replace(c.regexp, c.replacement), content));
49
- });
50
- return files;
58
+ const found = indices.filter(({ i }) => i > -1);
59
+ if (found.length === 0) {
60
+ return content;
61
+ }
62
+ const inBetween = (intervals, i) => {
63
+ for (const interval of intervals) {
64
+ for (const { start, end } of interval) {
65
+ if (i >= start && i <= end) {
66
+ return { start, end };
67
+ }
68
+ }
69
+ }
70
+ };
71
+ const getNotEmptyArray = (a, b) => a.length > 0 ? a : b;
72
+ const findCurrentReplace = (found) => {
73
+ const result = new Map();
74
+ for (const entry of found) {
75
+ const existing = result.get(entry.i);
76
+ if (!existing || entry.searchTerm.length >= existing.searchTerm.length) {
77
+ result.set(entry.i, entry);
78
+ }
79
+ }
80
+ return [...result.values()].sort((a, b) => a.i - b.i)[0];
81
+ };
82
+ // Ignore if search is in i18n key: replace "id" in "{{id.key}}" with
83
+ // "customer.id" and we need only the next one in string
84
+ found.forEach(index => index.inBetween = inBetween(ignoredStrings, index.i));
85
+ const foundToReplace = getNotEmptyArray(found.filter(index => !index.inBetween), found);
86
+ const currentReplace = findCurrentReplace(foundToReplace);
87
+ if (currentReplace.inBetween) {
88
+ start = currentReplace.inBetween.end;
89
+ }
90
+ else {
91
+ content = content.substring(0, currentReplace.i)
92
+ + currentReplace.replacement
93
+ + content.substring(currentReplace.i + currentReplace.searchTerm.length);
94
+ start = currentReplace.i + currentReplace.replacement.length;
95
+ }
96
+ }
51
97
  }
98
+ const findAllOccurrences = (string, substring, start) => {
99
+ if (!substring) {
100
+ return [];
101
+ }
102
+ const indices = [];
103
+ let index = start;
104
+ while ((index = string.indexOf(substring, index)) !== -1) {
105
+ indices.push(index);
106
+ index += substring.length; // shift from current finding
107
+ }
108
+ return indices;
109
+ };
52
110
  export function insertInArray(array, index, insert) {
53
111
  array.splice(index, 0, insert);
54
112
  }
@@ -64,9 +122,8 @@ export function writeTempAnnotations({ writeTempFiles }, name, language, content
64
122
  fs.writeFileSync(path.join(TEMP_DIST_FOLDER, name + ".xml"), content);
65
123
  }
66
124
  }
67
- export function removePropertiesExtension(filePath) {
68
- const lastIndexOf = filePath.lastIndexOf(".properties");
69
- return filePath.substring(0, lastIndexOf);
125
+ export function trimExtension(filePath) {
126
+ return filePath.replace(/\.[^/.]+$/, "");
70
127
  }
71
128
  export function traverse(json, paths, callback) {
72
129
  if (!json) {
@@ -101,6 +158,7 @@ export function logBuilderVersion() {
101
158
  const packageJson = fs.readFileSync(path.join(__dirname, "../../package.json"), { encoding: "utf-8" });
102
159
  const packageJsonVersion = JSON.parse(packageJson).version;
103
160
  log.info(`Running app-variant-bundler-build with version ${packageJsonVersion}`);
161
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
104
162
  }
105
163
  catch (e) {
106
164
  // do nothing
@@ -118,4 +176,19 @@ export function getUniqueName(existingNames, template) {
118
176
  } while (existingNames.includes(template + suffixString));
119
177
  return template + suffixString;
120
178
  }
179
+ export function getI18nPropertyKeys(files) {
180
+ const keys = new Set();
181
+ files.forEach((content, filename) => {
182
+ if (filename.endsWith(".properties")) {
183
+ const lines = content.split("\n").filter(line => !line.startsWith("#"));
184
+ for (const line of lines) {
185
+ const [key] = line.split("=");
186
+ if (key) {
187
+ keys.add(key);
188
+ }
189
+ }
190
+ }
191
+ });
192
+ return [...keys];
193
+ }
121
194
  //# sourceMappingURL=commonUtil.js.map
@@ -0,0 +1,17 @@
1
+ export default class FilesUtil {
2
+ static filter(files: ReadonlyMap<string, string>): ReadonlyMap<string, string>;
3
+ /**
4
+ * Renames files in the base application substituting original id with
5
+ * appVariant id. Renames all files except i18n properties files, when we
6
+ * rename property keys of nested application variants we got in the end
7
+ * multiple i18n properties with the same key, but different values. UI5
8
+ * takes the first one which is the oldest but we need the latest most
9
+ * recent updated one.
10
+ * @param files - The files of the base application.
11
+ * @param reference - The reference of the app variant (original/base app id).
12
+ * @param id - The id of the app variant.
13
+ * @returns A map of renamed files.
14
+ */
15
+ static rename(files: ReadonlyMap<string, string>, references: string[], adaptationProjectId: string): ReadonlyMap<string, string>;
16
+ private static ignore;
17
+ }
@@ -0,0 +1,49 @@
1
+ import { rename, getI18nPropertyKeys } from "./commonUtil.js";
2
+ import ManifestHandler from "./renamingHandlers/manifestHandler.js";
3
+ export default class FilesUtil {
4
+ static filter(files) {
5
+ const result = new Map();
6
+ files.forEach((content, filename) => {
7
+ if (!this.ignore(filename)) {
8
+ result.set(filename, content);
9
+ }
10
+ });
11
+ return result;
12
+ }
13
+ /**
14
+ * Renames files in the base application substituting original id with
15
+ * appVariant id. Renames all files except i18n properties files, when we
16
+ * rename property keys of nested application variants we got in the end
17
+ * multiple i18n properties with the same key, but different values. UI5
18
+ * takes the first one which is the oldest but we need the latest most
19
+ * recent updated one.
20
+ * @param files - The files of the base application.
21
+ * @param reference - The reference of the app variant (original/base app id).
22
+ * @param id - The id of the app variant.
23
+ * @returns A map of renamed files.
24
+ */
25
+ static rename(files, references, adaptationProjectId) {
26
+ const handlers = [new ManifestHandler()];
27
+ const IGNORE_EXTENSIONS = [".properties"];
28
+ const ignoreInStrings = getI18nPropertyKeys(files);
29
+ const renamedFiles = new Map();
30
+ for (const handler of handlers) {
31
+ handler.before(files);
32
+ }
33
+ files.forEach((content, filename) => {
34
+ if (!IGNORE_EXTENSIONS.some(ext => filename.endsWith(ext))) {
35
+ content = rename(content, references, adaptationProjectId, ignoreInStrings);
36
+ }
37
+ renamedFiles.set(filename, content);
38
+ });
39
+ for (const handler of handlers) {
40
+ handler.after(renamedFiles);
41
+ }
42
+ return renamedFiles;
43
+ }
44
+ static ignore(filename) {
45
+ const IGNORE_FILES = ["manifest.appdescr_variant"];
46
+ return filename.startsWith("changes/manifest/") || IGNORE_FILES.includes(filename);
47
+ }
48
+ }
49
+ //# sourceMappingURL=filesUtil.js.map
@@ -1,12 +1,19 @@
1
- import * as Resource from "@ui5/fs/Resource";
2
- import { IAppVariantInfo } from "../model/types.js";
3
- type Resource = typeof Resource;
4
- export default class I18NMerger {
5
- static analyzeAppVariantManifestChanges(rootFolder: string, tranlsationRegexPattern: string, { changes }: IAppVariantInfo): {
6
- mergePathsRegex: RegExp[];
7
- copyPathsRegex: RegExp[];
1
+ import AppVariant from "../appVariantManager.js";
2
+ import { IChange } from "../model/types.js";
3
+ export default class FileMerger {
4
+ static analyzeAppVariantManifestChanges(manifestChanges: ReadonlyArray<IChange>): {
5
+ mergePaths: RegExp[];
6
+ copyPaths: RegExp[];
8
7
  };
9
- static mergeI18NFiles(baseAppResources: any[], appVariantResources: any[], projectNamespace: string, baseAppManifestI18NPath: string, appVariantInfo: IAppVariantInfo, taskUtil: any): Promise<(typeof Resource)[]>;
8
+ static merge(baseAppFiles: ReadonlyMap<string, string>, i18nPath: string, appVariant: AppVariant): Map<string, string>;
9
+ /**
10
+ * Filters out specific lines from the given string.
11
+ * Removes lines matching:
12
+ * - __ldi.translation.uuid\s*=\s*(.*)
13
+ * - ABAP_TRANSLATION
14
+ * - SAPUI5 TRANSLATION-KEY
15
+ */
16
+ private static filterTranslationMetaLines;
10
17
  /**
11
18
  * Merge/Append base property file with property file from app variant
12
19
  * FIXME Currently merge could duplicate keys which causes undefined
@@ -16,16 +23,4 @@ export default class I18NMerger {
16
23
  * existing overwritten keys (as there should be none)
17
24
  */
18
25
  private static mergePropertiesFiles;
19
- /**
20
- * update the path of app variant property file so it will be copied into
21
- <app_variant_id> folder
22
- */
23
- private static moveToAppVarSubfolder;
24
- /**
25
- * create new i18n file in case e.g. translation file does not exist in base
26
- * app but in variant and copy of translation file is needed
27
- */
28
- private static createFile;
29
- private static mergeFiles;
30
26
  }
31
- export {};
@@ -1,19 +1,18 @@
1
- import { dotToUnderscore, escapeRegex, removePropertiesExtension } from "./commonUtil.js";
2
- import ResourceUtil from "./resourceUtil.js";
1
+ import { dotToUnderscore, escapeRegex, trimExtension } from "./commonUtil.js";
3
2
  import { posix as path } from "path";
4
- export default class I18NMerger {
5
- static analyzeAppVariantManifestChanges(rootFolder, tranlsationRegexPattern, { changes }) {
3
+ export default class FileMerger {
4
+ static analyzeAppVariantManifestChanges(manifestChanges) {
6
5
  // check which files need to be copied and which files need to be merged and copied
7
6
  // this is necessary because lrep does not support multiple enhanceWith with multiple locations
7
+ const TRANSLATION_REGEX_PATTERN = "((_[a-z]{2,3})?(_[a-zA-Z]{2,3}(_[a-zA-Z]{2,20})?)?)\.properties$";
8
8
  const mergePaths = new Set();
9
9
  const copyPaths = new Set();
10
- changes.forEach((change) => {
10
+ manifestChanges.forEach((change) => {
11
11
  const i18nPathWithExtension = change.content?.bundleUrl || change.texts?.i18n;
12
12
  if (i18nPathWithExtension) {
13
13
  // build regex to match specific + language related files
14
- const i18nPath = removePropertiesExtension(i18nPathWithExtension);
15
- const resourcePath = path.join(rootFolder, i18nPath);
16
- const regex = new RegExp(escapeRegex(resourcePath) + tranlsationRegexPattern);
14
+ const i18nPath = trimExtension(i18nPathWithExtension);
15
+ const regex = new RegExp("^" + escapeRegex(i18nPath) + TRANSLATION_REGEX_PATTERN);
17
16
  if (change.changeType.includes("addNewModelEnhanceWith")) {
18
17
  copyPaths.add(regex);
19
18
  }
@@ -22,36 +21,44 @@ export default class I18NMerger {
22
21
  }
23
22
  }
24
23
  });
25
- return { mergePathsRegex: [...mergePaths.values()], copyPathsRegex: [...copyPaths.values()] };
24
+ return { mergePaths: Array.from(mergePaths), copyPaths: Array.from(copyPaths) };
26
25
  }
27
- static async mergeI18NFiles(baseAppResources, appVariantResources, projectNamespace, baseAppManifestI18NPath, appVariantInfo, taskUtil) {
28
- const aggregatedResourceFilesMap = new Map(baseAppResources.map(baseAppResource => [baseAppResource.getPath(), baseAppResource]));
29
- const i18nTargetFolder = dotToUnderscore(appVariantInfo.id);
30
- const rootFolder = ResourceUtil.getRootFolder(projectNamespace);
31
- const tranlsationRegexPattern = "((_[a-z]{2,3})?(_[a-zA-Z]{2,3}(_[a-zA-Z]{2,20})?)?)\.properties$";
32
- const { copyPathsRegex: copyPathsValues, mergePathsRegex: mergePathsValues } = this.analyzeAppVariantManifestChanges(rootFolder, tranlsationRegexPattern, appVariantInfo);
33
- for (const appVariantResource of appVariantResources) {
34
- const appVariantResourcePath = appVariantResource.getPath();
35
- if (appVariantResourcePath.endsWith(".properties")) {
26
+ static merge(baseAppFiles, i18nPath, appVariant) {
27
+ const i18nTargetFolder = dotToUnderscore(appVariant.id);
28
+ const { copyPaths, mergePaths } = this.analyzeAppVariantManifestChanges(appVariant.getProcessedManifestChanges());
29
+ const files = new Map(baseAppFiles);
30
+ for (const [filename, content] of Array.from(appVariant.files)) {
31
+ if (filename.endsWith(".properties")) {
36
32
  // merge/copy logic
37
33
  // check if file matches with regex in merge/copy
38
- const mergePathMatch = mergePathsValues.map(path => appVariantResourcePath.match(path)).find(match => match);
39
- const shouldMergeFile = !!mergePathMatch;
40
- const shouldCopyFile = copyPathsValues.map(path => appVariantResourcePath.match(path)).find(match => match);
41
- if (shouldMergeFile) {
42
- let baseAppI18NPath = `${rootFolder}/${baseAppManifestI18NPath}${mergePathMatch[1] || ""}.properties`;
43
- await this.mergePropertiesFiles(aggregatedResourceFilesMap, appVariantResource, baseAppI18NPath);
34
+ const mergePathMatch = mergePaths.map(path => filename.match(path)).find(match => match);
35
+ const copyPathMatch = copyPaths.map(path => filename.match(path)).find(match => match);
36
+ if (mergePathMatch) {
37
+ this.mergePropertiesFiles(files, i18nPath, content, mergePathMatch[1]);
44
38
  }
45
- // Resource for to be copied file already exists so we only have to adjust path
46
- // Otherwise we have to omit it. We always change the path to avoid omitting a base app file
47
- this.moveToAppVarSubfolder(appVariantResource, rootFolder, i18nTargetFolder);
48
- if (!shouldCopyFile) {
49
- taskUtil.setTag(appVariantResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult, true);
39
+ if (copyPathMatch) {
40
+ files.set(path.join(i18nTargetFolder, filename), content);
50
41
  }
51
42
  }
52
- aggregatedResourceFilesMap.set(appVariantResource.getPath(), appVariantResource);
43
+ else {
44
+ files.set(filename, content);
45
+ }
53
46
  }
54
- return Array.from(aggregatedResourceFilesMap.values());
47
+ return files;
48
+ }
49
+ /**
50
+ * Filters out specific lines from the given string.
51
+ * Removes lines matching:
52
+ * - __ldi.translation.uuid\s*=\s*(.*)
53
+ * - ABAP_TRANSLATION
54
+ * - SAPUI5 TRANSLATION-KEY
55
+ */
56
+ static filterTranslationMetaLines(content) {
57
+ const lines = content.split('\n');
58
+ const filtered = lines.filter(line => !/^# __ldi\.translation\.uuid\s*=/.test(line) &&
59
+ !line.startsWith("# ABAP_TRANSLATION") &&
60
+ !line.startsWith("# SAPUI5 TRANSLATION-KEY"));
61
+ return filtered.join('\n');
55
62
  }
56
63
  /**
57
64
  * Merge/Append base property file with property file from app variant
@@ -61,39 +68,14 @@ export default class I18NMerger {
61
68
  * app variant Id as prefix => If we filter on them we do not need to remove
62
69
  * existing overwritten keys (as there should be none)
63
70
  */
64
- static async mergePropertiesFiles(aggregatedResourceFilesMap, variantResource, baseAppI18NPath) {
65
- const baseAppI18NFile = aggregatedResourceFilesMap.get(baseAppI18NPath);
66
- if (baseAppI18NFile) {
67
- await this.mergeFiles(baseAppI18NFile, variantResource);
68
- }
69
- else {
70
- // create the merge target file if it missing in base app. Maybe the language does not exist in the base app.
71
- // Since the file might also be copied we do not just change the path of it but create another resource
72
- await this.createFile(aggregatedResourceFilesMap, baseAppI18NPath, variantResource);
73
- }
74
- }
75
- /**
76
- * update the path of app variant property file so it will be copied into
77
- <app_variant_id> folder
78
- */
79
- static moveToAppVarSubfolder(variantResource, rootFolder, i18nBundleName) {
80
- const relativeFilePath = variantResource.getPath().substring(rootFolder.length);
81
- const newResourcePath = path.join(rootFolder, i18nBundleName, relativeFilePath);
82
- variantResource.setPath(newResourcePath);
83
- }
84
- /**
85
- * create new i18n file in case e.g. translation file does not exist in base
86
- * app but in variant and copy of translation file is needed
87
- */
88
- static async createFile(aggregatedResourceFilesMap, path, resource) {
89
- const createdFile = await resource.clone();
90
- createdFile.setPath(path);
91
- aggregatedResourceFilesMap.set(path, createdFile);
92
- }
93
- static async mergeFiles(baseFile, variantFile) {
94
- const variantFileContent = await variantFile.getString();
95
- const mergedFileContent = await baseFile.getString();
96
- baseFile.setString(`${mergedFileContent}\n\n#App variant specific text file\n\n${variantFileContent}`);
71
+ static mergePropertiesFiles(files, i18nPath, appVariantFileContent, language = "") {
72
+ const baseAppI18nPath = i18nPath + language + ".properties";
73
+ const baseAppFileContent = files.get(baseAppI18nPath);
74
+ const filteredBaseContent = baseAppFileContent ? this.filterTranslationMetaLines(baseAppFileContent) : "";
75
+ const content = filteredBaseContent
76
+ ? `${filteredBaseContent}\n\n#App variant specific text file\n\n${appVariantFileContent}`
77
+ : appVariantFileContent;
78
+ files.set(baseAppI18nPath, content);
97
79
  }
98
80
  }
99
81
  //# sourceMappingURL=i18nMerger.js.map
@@ -0,0 +1,6 @@
1
+ import { IRenamingHandler } from "./renamingHandler.js";
2
+ export default class ManifestHandler implements IRenamingHandler {
3
+ private appVariantIdHierarchy;
4
+ before(files: ReadonlyMap<string, string>): void;
5
+ after(files: Map<string, string>): void;
6
+ }
@@ -0,0 +1,20 @@
1
+ export default class ManifestHandler {
2
+ appVariantIdHierarchy = [];
3
+ before(files) {
4
+ const manifest = files.get("manifest.json");
5
+ if (manifest) {
6
+ const manifestJson = JSON.parse(manifest);
7
+ this.appVariantIdHierarchy = manifestJson["sap.ui5"].appVariantIdHierarchy;
8
+ }
9
+ }
10
+ after(files) {
11
+ const manifest = files.get("manifest.json");
12
+ if (manifest) {
13
+ const manifestJson = JSON.parse(manifest);
14
+ manifestJson["sap.ui5"].appVariantIdHierarchy = this.appVariantIdHierarchy;
15
+ files.set("manifest.json", JSON.stringify(manifestJson));
16
+ }
17
+ }
18
+ ;
19
+ }
20
+ //# sourceMappingURL=manifestHandler.js.map
@@ -0,0 +1,4 @@
1
+ export interface IRenamingHandler {
2
+ before(files: ReadonlyMap<string, string>): void;
3
+ after(files: Map<string, string>): void;
4
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=renamingHandler.js.map
@@ -1,6 +1,7 @@
1
+ import { AxiosRequestConfig } from "axios";
1
2
  export default class RequestUtil {
2
3
  static head(url: string): Promise<any>;
3
4
  static get(url: string, options?: any): Promise<any>;
4
- static request(url: string, method: Function, options?: any): Promise<any>;
5
+ static request(url: string, method: (url: string, config?: AxiosRequestConfig) => any, options?: any): Promise<any>;
5
6
  private static handleError;
6
7
  }
@@ -3,9 +3,11 @@ export default class ResourceUtil {
3
3
  static relativeToRoot(resourcePath: string, projectNamespace?: string): string;
4
4
  static getResourcePath(projectNamespace?: string, ...paths: string[]): string;
5
5
  static write(dir: string, files: Map<string, string>): Promise<void[]>;
6
- static read(rootFolder: string, folder: string, files: Map<string, string>, exclude?: string[]): void;
6
+ private static _read;
7
+ static read(folder: string): Map<string, string>;
7
8
  static getString(resource: any): Promise<string>;
8
9
  static getJson(resource: any): Promise<any>;
9
10
  static setString(resource: any, str: string): void;
10
11
  static createResource(filename: string, projectNamespace: string, content: string): any;
12
+ static toFileMap(resources: ReadonlyArray<Resource>, projectNamespace: string): Promise<Map<string, string>>;
11
13
  }
@@ -29,7 +29,7 @@ export default class ResourceUtil {
29
29
  });
30
30
  return Promise.all(promises);
31
31
  }
32
- static read(rootFolder, folder, files, exclude = []) {
32
+ static _read(rootFolder, folder, files, exclude = []) {
33
33
  const entries = fs.readdirSync(folder);
34
34
  for (let entry of entries) {
35
35
  const entryPath = path.join(folder, entry);
@@ -39,10 +39,15 @@ export default class ResourceUtil {
39
39
  files.set(normalized, fs.readFileSync(entryPath, { encoding: "utf-8" }));
40
40
  }
41
41
  else if (stats.isDirectory()) {
42
- this.read(rootFolder, entryPath, files, exclude);
42
+ this._read(rootFolder, entryPath, files, exclude);
43
43
  }
44
44
  }
45
45
  }
46
+ static read(folder) {
47
+ const files = new Map();
48
+ this._read(folder, folder, files);
49
+ return files;
50
+ }
46
51
  static getString(resource) {
47
52
  return resource.getBuffer().then((buffer) => buffer.toString(UTF8));
48
53
  }
@@ -58,5 +63,13 @@ export default class ResourceUtil {
58
63
  string: content
59
64
  });
60
65
  }
66
+ static async toFileMap(resources, projectNamespace) {
67
+ const files = new Map();
68
+ const rootFolderLength = ResourceUtil.getRootFolder(projectNamespace).length;
69
+ for (const resource of resources) {
70
+ files.set(resource.getPath().substring(rootFolderLength + 1), await ResourceUtil.getString(resource));
71
+ }
72
+ return files;
73
+ }
61
74
  }
62
75
  //# sourceMappingURL=resourceUtil.js.map