@ui5/builder 3.5.1 → 4.0.1

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 (32) hide show
  1. package/CHANGELOG.md +47 -3
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +6 -6
  4. package/jsdoc.json +7 -2
  5. package/lib/lbt/bundle/Builder.js +155 -113
  6. package/lib/lbt/bundle/ResolvedBundleDefinition.js +11 -17
  7. package/lib/processors/bundlers/moduleBundler.js +19 -8
  8. package/lib/processors/jsdoc/apiIndexGenerator.js +1 -1
  9. package/lib/processors/jsdoc/jsdocGenerator.js +4 -11
  10. package/lib/processors/jsdoc/lib/package.json +3 -0
  11. package/lib/processors/jsdoc/lib/ui5/template/{publish.cjs → publish.js} +1 -1
  12. package/lib/processors/jsdoc/sdkTransformer.js +1 -1
  13. package/lib/processors/manifestEnhancer.js +425 -0
  14. package/lib/processors/minifier.js +5 -1
  15. package/lib/tasks/buildThemes.js +4 -0
  16. package/lib/tasks/bundlers/generateBundle.js +11 -2
  17. package/lib/tasks/bundlers/generateComponentPreload.js +15 -8
  18. package/lib/tasks/bundlers/generateFlexChangesBundle.js +1 -2
  19. package/lib/tasks/bundlers/generateLibraryPreload.js +11 -8
  20. package/lib/tasks/bundlers/generateStandaloneAppBundle.js +40 -21
  21. package/lib/tasks/bundlers/utils/applyDefaultsToBundleDefinition.js +35 -0
  22. package/lib/tasks/enhanceManifest.js +32 -0
  23. package/lib/tasks/generateCachebusterInfo.js +1 -2
  24. package/lib/tasks/generateThemeDesignerResources.js +1 -2
  25. package/lib/tasks/generateVersionInfo.js +3 -1
  26. package/lib/tasks/taskRepository.js +2 -1
  27. package/lib/tasks/transformBootstrapHtml.js +2 -3
  28. package/package.json +20 -21
  29. /package/lib/processors/jsdoc/lib/{createIndexFiles.cjs → createIndexFiles.js} +0 -0
  30. /package/lib/processors/jsdoc/lib/{transformApiJson.cjs → transformApiJson.js} +0 -0
  31. /package/lib/processors/jsdoc/lib/ui5/{plugin.cjs → plugin.js} +0 -0
  32. /package/lib/processors/jsdoc/lib/ui5/template/utils/{versionUtil.cjs → versionUtil.js} +0 -0
@@ -33,10 +33,10 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
33
33
  * the ui5loader is available.
34
34
  * </li>
35
35
  * <li>
36
- * <code>require</code>: A 'require' section is transformed into a sequence of jQuery.sap.require calls. The
37
- * list will be resolved like an include pattern list in any of the other sections and for each of the resolved
38
- * modules, a jQuery.sap.require will be created. In case the ui5loader is available, 'sap.ui.requireSync' is
39
- * used instead.
36
+ * <code>require</code>: A `require` section is transformed into a `sap.ui.require` call with all the dependencies
37
+ * resolved. This module comes with an `async` flag. When set to false, the modules
38
+ * are loaded using `sap.ui.requireSync` instead of `sap.ui.require`.
39
+ * **Note:** The `sap.ui.requireSync` API is not available in UI5 version 2.x.
40
40
  * </li>
41
41
  * <li>
42
42
  * <code>bundleInfo</code>: A 'bundleInfo' section describes the content of another named bundle. This information
@@ -75,6 +75,11 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
75
75
  * @property {boolean} [declareRawModules=false] Whether raw modules should be declared after jQuery.sap.global
76
76
  * became available. With the usage of the ui5loader, this flag should be set to 'false'
77
77
  * @property {boolean} [sort=true] Whether the modules should be sorted by their dependencies
78
+ * @property {boolean} [async=true] Whether the `require` section of the module should be loaded asynchronously.
79
+ * When set to true, the modules are loaded using a single `sap.ui.require` call instead of multiple
80
+ * `sap.ui.requireSync` calls.
81
+ * The latter API is not available in UI5 version 2.x.
82
+ * **Note:** This property is available only for `mode=require`.
78
83
  */
79
84
 
80
85
  /* eslint-disable max-len */
@@ -101,7 +106,6 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
101
106
  * with an optimization marker
102
107
  * @property {boolean} [addTryCatchRestartWrapper=false] Whether to wrap bootable bundles with
103
108
  * a try/catch to filter out "Restart" errors
104
- * @property {boolean} [usePredefineCalls=false] If set to 'true', sap.ui.predefine is used for UI5 modules
105
109
  * @property {number} [numberOfParts=1] The number of parts the module bundle should be splitted
106
110
  * @property {boolean} [ignoreMissingModules=false] When searching for modules which are optional for further
107
111
  * processing, do not throw in case they are missing
@@ -133,26 +137,33 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
133
137
  bundle definition
134
138
  * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [parameters.options.bundleOptions] Module
135
139
  bundle options
140
+ * @param {string} [parameters.options.targetUi5CoreVersion] Optional semver compliant sap.ui.core project version, e.g '2.0.0'.
141
+ This allows the bundler to make assumptions on available runtime APIs.
142
+ Omit if the ultimate UI5 version at runtime is unknown or can't be determined.
143
+ * @param {boolean} [parameters.options.allowStringBundling=false] Optional flag to allow bundling of modules as a string.
136
144
  * @returns {Promise<module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundlerResult[]>}
137
145
  * Promise resolving with module bundle resources
138
146
  */
139
147
  /* eslint-enable max-len */
140
- export default function({resources, options: {bundleDefinition, bundleOptions, moduleNameMapping}}) {
148
+ export default function({resources, options: {
149
+ bundleDefinition, bundleOptions, moduleNameMapping, targetUi5CoreVersion, allowStringBundling = false
150
+ }}) {
141
151
  // Apply defaults without modifying the passed object
142
152
  bundleOptions = Object.assign({}, {
143
153
  optimize: true,
144
154
  sourceMap: true,
145
155
  decorateBootstrapModule: false,
146
156
  addTryCatchRestartWrapper: false,
147
- usePredefineCalls: false,
148
157
  numberOfParts: 1,
149
158
  ignoreMissingModules: false
150
159
  }, bundleOptions);
151
160
 
161
+ // bundleDefinition's defaults get applied in the corresponding standard tasks
162
+
152
163
  const pool = new LocatorResourcePool({
153
164
  ignoreMissingModules: bundleOptions.ignoreMissingModules
154
165
  });
155
- const builder = new BundleBuilder(pool);
166
+ const builder = new BundleBuilder(pool, targetUi5CoreVersion, allowStringBundling);
156
167
 
157
168
  if (log.isLevelEnabled("verbose")) {
158
169
  log.verbose(`Generating bundle:`);
@@ -1,5 +1,5 @@
1
1
  import {createResource} from "@ui5/fs/resourceFactory";
2
- import createIndex from "./lib/createIndexFiles.cjs";
2
+ import createIndex from "./lib/createIndexFiles.js";
3
3
 
4
4
  /**
5
5
  * @public
@@ -4,12 +4,8 @@ import path from "node:path";
4
4
  import {promisify} from "node:util";
5
5
  const writeFile = promisify(fs.writeFile);
6
6
  import {createAdapter} from "@ui5/fs/resourceFactory";
7
- import {createRequire} from "node:module";
8
7
  import {fileURLToPath} from "node:url";
9
8
 
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const require = createRequire(import.meta.url);
12
-
13
9
  /**
14
10
  * @public
15
11
  * @module @ui5/builder/processors/jsdoc/jsdocGenerator
@@ -93,12 +89,9 @@ async function generateJsdocConfig({targetPath, tmpPath, namespace, projectName,
93
89
  const backslashRegex = /\\/g;
94
90
 
95
91
  // Resolve path to this script to get the path to the JSDoc extensions folder
96
- const jsdocPath = path.normalize(__dirname);
97
- const pluginPath = path.join(jsdocPath, "lib", "ui5", "plugin.cjs").replace(backslashRegex, "\\\\");
98
- // Using export via package.json to allow loading the CJS template.
99
- // jsdoc appends /publish to the provided path but doesn't allow to
100
- // add the .cjs extension, so loading won't work otherwise.
101
- const templatePath = "@ui5/builder/internal/jsdoc/template";
92
+ const jsdocPath = path.normalize(import.meta.dirname);
93
+ const pluginPath = path.join(jsdocPath, "lib", "ui5", "plugin.js").replace(backslashRegex, "\\\\");
94
+ const templatePath = path.join(jsdocPath, "lib", "ui5", "template").replace(backslashRegex, "\\\\");
102
95
  const destinationPath = path.normalize(tmpPath).replace(backslashRegex, "\\\\");
103
96
  const jsapiFilePath = path.join(targetPath, "libraries", projectName + ".js").replace(backslashRegex, "\\\\");
104
97
  const apiJsonFolderPath = path.join(tmpPath, "dependency-apis").replace(backslashRegex, "\\\\");
@@ -164,7 +157,7 @@ async function writeJsdocConfig(targetDirPath, config) {
164
157
  */
165
158
  async function buildJsdoc({sourcePath, configPath}) {
166
159
  const args = [
167
- require.resolve("jsdoc/jsdoc"),
160
+ fileURLToPath(import.meta.resolve("jsdoc/jsdoc.js")),
168
161
  "-c",
169
162
  configPath,
170
163
  "--verbose",
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -26,7 +26,7 @@ const info = logger.info.bind(logger);
26
26
  const warning = logger.warn.bind(logger);
27
27
  const error = logger.error.bind(logger);
28
28
 
29
- const {extractVersion, extractSince} = require("./utils/versionUtil.cjs");
29
+ const {extractVersion, extractSince} = require("./utils/versionUtil");
30
30
 
31
31
  /* errors that might fail the build in future */
32
32
  function future(msg) {
@@ -1,5 +1,5 @@
1
1
  import {createResource} from "@ui5/fs/resourceFactory";
2
- import transformer from "./lib/transformApiJson.cjs";
2
+ import transformer from "./lib/transformApiJson.js";
3
3
 
4
4
  /**
5
5
  * @public
@@ -0,0 +1,425 @@
1
+ import semver from "semver";
2
+ const {SemVer: Version, lt} = semver;
3
+ import {promisify} from "node:util";
4
+ import path from "node:path/posix";
5
+ import {getLogger} from "@ui5/logger";
6
+ const log = getLogger("builder:processors:manifestEnhancer");
7
+
8
+ const APP_DESCRIPTOR_V22 = new Version("1.21.0");
9
+
10
+ function isAbsoluteUrl(url) {
11
+ if (url.startsWith("/")) {
12
+ return true;
13
+ }
14
+ try {
15
+ const parsedUrl = new URL(url);
16
+ // URL with ui5 protocol shouldn't be treated as absolute URL and will be handled separately
17
+ return parsedUrl.protocol !== "ui5:";
18
+ } catch (err) {
19
+ // URL constructor without base requires absolute URL and throws an error for relative URLs
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Returns a bundle URL from the given bundle name, relative to the given namespace.
26
+ *
27
+ * @param {string} bundleName Bundle name (e.g. "sap.ui.demo.app.i18n.i18n") to be resolved to a relative URL
28
+ * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app")
29
+ * to which a bundleName should be resolved to
30
+ * @returns {string} Relative bundle URL (e.g. "i18n/i18n.properties")
31
+ */
32
+ function getRelativeBundleUrlFromName(bundleName, sapAppId) {
33
+ const bundleUrl = "/resources/" + bundleName.replace(/\./g, "/") + ".properties";
34
+ return normalizeBundleUrl(bundleUrl, sapAppId);
35
+ }
36
+
37
+ // Copied from sap/base/util/LoaderExtensions.resolveUI5Url
38
+ // Adjusted to not resolve the URL, but create an absolute path prefixed with /resources
39
+ function resolveUI5Url(sUrl) {
40
+ // check for ui5 scheme
41
+ if (sUrl.startsWith("ui5:")) {
42
+ let sNoScheme = sUrl.replace("ui5:", "");
43
+
44
+ // check for authority
45
+ if (!sNoScheme.startsWith("//")) {
46
+ // URLs using the 'ui5' protocol must be absolute.
47
+ // Relative and server absolute URLs are reserved for future use.
48
+ return null;
49
+ }
50
+
51
+ sNoScheme = sNoScheme.replace("//", "");
52
+
53
+ return "/resources/" + sNoScheme;
54
+ } else {
55
+ // not a ui5 url
56
+ return sUrl;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Normalizes a bundle URL relative to the project namespace.
62
+ *
63
+ * @param {string} bundleUrl Relative bundle URL to be normalized
64
+ * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app")
65
+ * to which the URL is relative to
66
+ * @returns {string} Normalized relative bundle URL (e.g. "i18n/i18n.properties")
67
+ */
68
+ function normalizeBundleUrl(bundleUrl, sapAppId) {
69
+ // Create absolute path with namespace from sap.app/id
70
+ const absoluteNamespace = `/resources/${sapAppId.replaceAll(/\./g, "/")}`;
71
+
72
+ const resolvedAbsolutePath = path.resolve(absoluteNamespace, bundleUrl);
73
+ const resolvedRelativePath = path.relative(absoluteNamespace, resolvedAbsolutePath);
74
+ return resolvedRelativePath;
75
+ }
76
+
77
+ /**
78
+ * Returns the bundle URL from the given bundle configuration.
79
+ *
80
+ * @param {object} bundleConfig Bundle configuration
81
+ * @param {string} sapAppId Project namespace from sap.app/id (e.g. "sap.ui.demo.app")
82
+ * to which a bundleName should be resolved to
83
+ * @param {string} [defaultBundleUrl] Default bundle url in case bundleConfig is not defined
84
+ */
85
+ function getBundleUrlFromConfig(bundleConfig, sapAppId, defaultBundleUrl) {
86
+ if (!bundleConfig) {
87
+ // Use default URL (or undefined if argument is not provided)
88
+ return defaultBundleUrl;
89
+ } else if (typeof bundleConfig === "string") {
90
+ return bundleConfig;
91
+ } else if (typeof bundleConfig === "object") {
92
+ return getBundleUrlFromConfigObject(bundleConfig, sapAppId);
93
+ }
94
+ }
95
+
96
+ // Same as above, but does only accept objects, not strings or defaults
97
+ function getBundleUrlFromConfigObject(bundleConfig, sapAppId, fallbackBundleUrl) {
98
+ if (typeof bundleConfig === "object") {
99
+ if (bundleConfig.bundleName) {
100
+ return getRelativeBundleUrlFromName(bundleConfig.bundleName, sapAppId);
101
+ } else if (bundleConfig.bundleUrl) {
102
+ return bundleConfig.bundleUrl;
103
+ }
104
+ }
105
+ return fallbackBundleUrl;
106
+ }
107
+
108
+ // See runtime logic in sap/ui/core/Lib#_normalizeI18nSettings
109
+ function getBundleUrlFromSapUi5LibraryI18n(vI18n) {
110
+ if (vI18n == null || vI18n === true) {
111
+ return "messagebundle.properties";
112
+ } else if (typeof vI18n === "string") {
113
+ return vI18n;
114
+ } else if (typeof vI18n === "object") {
115
+ return vI18n.bundleUrl;
116
+ } else {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ class ManifestEnhancer {
122
+ /**
123
+ * @param {string} manifest manifest.json content
124
+ * @param {string} filePath manifest.json file path
125
+ * @param {fs} fs Node fs or custom [fs interface]{@link module:@ui5/fs/fsInterface}
126
+ */
127
+ constructor(manifest, filePath, fs) {
128
+ this.fsReadDir = promisify(fs.readdir);
129
+ this.cwd = path.dirname(filePath);
130
+ this.filePath = filePath;
131
+ this.manifest = JSON.parse(manifest);
132
+
133
+ this.isModified = false;
134
+ this.runInvoked = false;
135
+ }
136
+
137
+ markModified() {
138
+ this.isModified = true;
139
+ }
140
+
141
+ async readdir(relativePath) {
142
+ const absolutePath = path.resolve(this.cwd, relativePath);
143
+ try {
144
+ return await this.fsReadDir(absolutePath);
145
+ } catch (err) {
146
+ if (err?.code === "ENOENT") {
147
+ return [];
148
+ } else {
149
+ throw err;
150
+ }
151
+ }
152
+ }
153
+
154
+ async findSupportedLocales(i18nBundleUrl) {
155
+ const i18nBundleName = path.basename(i18nBundleUrl, ".properties");
156
+ const i18nBundlePrefix = `${i18nBundleName}_`;
157
+ const i18nBundleDir = path.dirname(i18nBundleUrl);
158
+ const i18nBundleFiles = await this.readdir(i18nBundleDir);
159
+ const supportedLocales = [];
160
+ i18nBundleFiles.forEach((fileName) => {
161
+ if (!fileName.endsWith(".properties")) {
162
+ return;
163
+ }
164
+ const fileNameWithoutExtension = path.basename(fileName, ".properties");
165
+ if (fileNameWithoutExtension === i18nBundleName) {
166
+ supportedLocales.push("");
167
+ } else if (fileNameWithoutExtension.startsWith(i18nBundlePrefix)) {
168
+ const locale = fileNameWithoutExtension.replace(i18nBundlePrefix, "");
169
+ supportedLocales.push(locale);
170
+ }
171
+ });
172
+ return supportedLocales.sort();
173
+ }
174
+
175
+ async processBundleConfig({bundleConfig, fallbackBundleUrl, isTerminologyBundle = false, fallbackLocale}) {
176
+ const bundleUrl = getBundleUrlFromConfigObject(bundleConfig, this.manifest["sap.app"].id, fallbackBundleUrl);
177
+ if (!bundleUrl) {
178
+ return;
179
+ }
180
+ if (bundleConfig.supportedLocales) {
181
+ return;
182
+ }
183
+
184
+ const supportedLocales = await this.getSupportedLocales(
185
+ bundleUrl, fallbackLocale ?? bundleConfig.fallbackLocale, isTerminologyBundle
186
+ );
187
+ if (supportedLocales.length > 0) {
188
+ bundleConfig.supportedLocales = supportedLocales;
189
+ this.markModified();
190
+ }
191
+ }
192
+
193
+ async getSupportedLocales(bundleUrl, fallbackLocale, isTerminologyBundle = false) {
194
+ // Ignore absolute URLs
195
+ if (isAbsoluteUrl(bundleUrl)) {
196
+ return [];
197
+ }
198
+ const resolvedBundleUrl = resolveUI5Url(bundleUrl);
199
+ if (!resolvedBundleUrl) {
200
+ // In case of a relative ui5-protocol URL
201
+ return [];
202
+ }
203
+ const sapAppId = this.manifest["sap.app"].id;
204
+ const normalizedBundleUrl = normalizeBundleUrl(resolvedBundleUrl, sapAppId);
205
+ if (normalizedBundleUrl.startsWith("../")) {
206
+ log.verbose(
207
+ `${this.filePath}: ` +
208
+ `bundleUrl '${bundleUrl}' points to a bundle outside of the ` +
209
+ `current namespace '${sapAppId}', enhancement of 'supportedLocales' is skipped`
210
+ );
211
+ return [];
212
+ }
213
+ const supportedLocales = await this.findSupportedLocales(normalizedBundleUrl);
214
+ if (!isTerminologyBundle && supportedLocales.length > 0) {
215
+ if (typeof fallbackLocale === "string" && !supportedLocales.includes(fallbackLocale)) {
216
+ log.error(
217
+ `${this.filePath}: ` +
218
+ `Generated supported locales ('${supportedLocales.join("', '")}') for ` +
219
+ `bundle '${normalizedBundleUrl}' ` +
220
+ "not containing the defined fallback locale '" + fallbackLocale + "'. Either provide a " +
221
+ "properties file for defined fallbackLocale or configure another available fallbackLocale"
222
+ );
223
+ return [];
224
+ } else if (typeof fallbackLocale === "undefined" && !supportedLocales.includes("en")) {
225
+ log.warn(
226
+ `${this.filePath}: ` +
227
+ `Generated supported locales ('${supportedLocales.join("', '")}') for ` +
228
+ `bundle '${normalizedBundleUrl}' ` +
229
+ "do not contain default fallback locale 'en'. Either provide a " +
230
+ "properties file for 'en' or configure another available fallbackLocale"
231
+ );
232
+ }
233
+ }
234
+ return supportedLocales;
235
+ }
236
+
237
+ async processSapAppI18n() {
238
+ const sapApp = this.manifest["sap.app"];
239
+ let sapAppI18n = sapApp.i18n;
240
+
241
+ // Process enhanceWith bundles first, as they check for an existing supportedLocales property
242
+ // defined by the developer, but not the one generated by the tooling.
243
+ await this.processTerminologiesAndEnhanceWith(sapAppI18n);
244
+
245
+ const i18nBundleUrl = getBundleUrlFromConfig(sapAppI18n, sapApp.id, "i18n/i18n.properties");
246
+
247
+ if (!sapAppI18n?.supportedLocales && i18nBundleUrl) {
248
+ const supportedLocales = await this.getSupportedLocales(i18nBundleUrl, sapAppI18n?.fallbackLocale);
249
+ if (supportedLocales.length > 0) {
250
+ if (!sapAppI18n || typeof sapAppI18n === "string") {
251
+ sapAppI18n = sapApp.i18n = {
252
+ bundleUrl: i18nBundleUrl
253
+ };
254
+ }
255
+ sapAppI18n.supportedLocales = supportedLocales;
256
+ this.markModified();
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Processes the terminologies and enhanceWith bundles of a bundle configuration.
263
+ *
264
+ * @param {object} bundleConfig
265
+ */
266
+ async processTerminologiesAndEnhanceWith(bundleConfig) {
267
+ const bundleConfigs = [];
268
+ const terminologyBundleConfigs = [];
269
+
270
+ if (bundleConfig?.terminologies) {
271
+ terminologyBundleConfigs.push(...Object.values(bundleConfig.terminologies));
272
+ }
273
+
274
+ bundleConfig?.enhanceWith?.forEach((config) => {
275
+ // The runtime logic propagates supportedLocales information to the enhanceWith bundles.
276
+ // In order to not break existing behavior, we do not generate supportedLocales for enhanceWith bundles
277
+ // in case the parent bundle does have supportedLocales defined.
278
+ if (!bundleConfig.supportedLocales) {
279
+ bundleConfigs.push({config, fallbackLocale: bundleConfig.fallbackLocale});
280
+ }
281
+ if (config.terminologies) {
282
+ terminologyBundleConfigs.push(...Object.values(config.terminologies));
283
+ }
284
+ });
285
+
286
+ await Promise.all(
287
+ bundleConfigs.map(({config, fallbackLocale}) => this.processBundleConfig({
288
+ bundleConfig: config,
289
+ fallbackLocale
290
+ }))
291
+ );
292
+ await Promise.all(
293
+ terminologyBundleConfigs.map((bundleConfig) => this.processBundleConfig({
294
+ bundleConfig, isTerminologyBundle: true
295
+ }))
296
+ );
297
+ }
298
+
299
+ async processSapUi5Models() {
300
+ const sapUi5Models = this.manifest["sap.ui5"]?.models;
301
+ if (typeof sapUi5Models !== "object") {
302
+ return;
303
+ }
304
+ const modelConfigs = Object.values(sapUi5Models)
305
+ .filter((modelConfig) => modelConfig.type === "sap.ui.model.resource.ResourceModel");
306
+
307
+ await Promise.all(
308
+ modelConfigs.map(async (modelConfig) => {
309
+ // Process enhanceWith bundles first, as they check for an existing supportedLocales property
310
+ // defined by the developer, but not the one generated by the tooling.
311
+ await this.processTerminologiesAndEnhanceWith(modelConfig.settings);
312
+
313
+ // Fallback to empty settings object in case only a "uri" is defined which will be converted
314
+ // to a settings object at runtime.
315
+ const settings = modelConfig.settings || {};
316
+
317
+ // Ensure to pass the "uri" property as fallback bundle URL according to the runtime logic.
318
+ // It is only taken into account if no "bundleUrl" or "bundleName" is defined.
319
+ await this.processBundleConfig({bundleConfig: settings, fallbackBundleUrl: modelConfig.uri});
320
+
321
+ // Ensure that the settings object is assigned back to the modelConfig
322
+ // in case it didn't existing before.
323
+ if (!modelConfig.settings) {
324
+ modelConfig.settings = settings;
325
+ }
326
+ })
327
+ );
328
+ }
329
+
330
+ async processSapUi5LibraryI18n() {
331
+ let sapUi5LibraryI18n = this.manifest["sap.ui5"]?.library?.i18n;
332
+
333
+ // Process enhanceWith bundles first, as they check for an existing supportedLocales property
334
+ // defined by the developer, but not the one generated by the tooling.
335
+ await this.processTerminologiesAndEnhanceWith(sapUi5LibraryI18n);
336
+
337
+ const i18nBundleUrl = getBundleUrlFromSapUi5LibraryI18n(sapUi5LibraryI18n);
338
+ if (i18nBundleUrl && !sapUi5LibraryI18n?.supportedLocales) {
339
+ const supportedLocales = await this.getSupportedLocales(i18nBundleUrl, sapUi5LibraryI18n?.fallbackLocale);
340
+ if (supportedLocales.length > 0) {
341
+ if (!sapUi5LibraryI18n || typeof sapUi5LibraryI18n !== "object") {
342
+ this.manifest["sap.ui5"] ??= {};
343
+ this.manifest["sap.ui5"].library ??= {};
344
+ sapUi5LibraryI18n = this.manifest["sap.ui5"].library.i18n = {
345
+ bundleUrl: i18nBundleUrl
346
+ };
347
+ }
348
+ sapUi5LibraryI18n.supportedLocales = supportedLocales;
349
+ this.markModified();
350
+ }
351
+ }
352
+ }
353
+
354
+ async run() {
355
+ // Prevent multiple invocations
356
+ if (this.runInvoked) {
357
+ throw new Error("ManifestEnhancer#run can only be invoked once per instance");
358
+ }
359
+ this.runInvoked = true;
360
+
361
+ if (!this.manifest._version) {
362
+ log.verbose(`${this.filePath}: _version is not defined. No supportedLocales can be generated`);
363
+ return;
364
+ }
365
+
366
+ if (lt(this.manifest._version, APP_DESCRIPTOR_V22)) {
367
+ log.verbose(`${this.filePath}: _version is lower than 1.21.0 so no supportedLocales can be generated`);
368
+ return;
369
+ }
370
+
371
+ if (!this.manifest["sap.app"]?.id) {
372
+ log.verbose(`${this.filePath}: sap.app/id is not defined. No supportedLocales can be generated`);
373
+ return;
374
+ }
375
+
376
+ if (this.manifest["sap.app"].type === "library") {
377
+ await this.processSapUi5LibraryI18n();
378
+ } else {
379
+ await Promise.all([
380
+ this.processSapAppI18n(),
381
+ this.processSapUi5Models()
382
+ ]);
383
+ }
384
+
385
+ if (this.isModified) {
386
+ return this.manifest;
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * @module @ui5/builder/processors/manifestEnhancer
393
+ */
394
+
395
+ /**
396
+ * Enriches the content of the manifest.json file.
397
+ *
398
+ * @public
399
+ * @function default
400
+ * @static
401
+ *
402
+ * @param {object} parameters Parameters
403
+ * @param {@ui5/fs/Resource[]} parameters.resources List of manifest.json resources to be processed
404
+ * @param {fs|module:@ui5/fs/fsInterface} parameters.fs Node fs or custom
405
+ * [fs interface]{@link module:@ui5/fs/fsInterface}.
406
+ * @returns {Promise<Array<@ui5/fs/Resource|undefined>>} Promise resolving with an array of modified resources
407
+ */
408
+ export default async function({resources, fs}) {
409
+ const res = await Promise.all(
410
+ resources.map(async (resource) => {
411
+ const manifest = await resource.getString();
412
+ const filePath = resource.getPath();
413
+ const manifestEnhancer = new ManifestEnhancer(manifest, filePath, fs);
414
+ const enrichedManifest = await manifestEnhancer.run();
415
+ if (enrichedManifest) {
416
+ resource.setString(JSON.stringify(enrichedManifest, null, 2));
417
+ return resource;
418
+ }
419
+ })
420
+ );
421
+ return res.filter(($) => $);
422
+ }
423
+
424
+ export const __internals__ = (process.env.NODE_ENV === "test") ?
425
+ {ManifestEnhancer, getRelativeBundleUrlFromName, normalizeBundleUrl, resolveUI5Url} : undefined;
@@ -42,6 +42,10 @@ function getPool(taskUtil) {
42
42
  let {idleWorkers, totalWorkers} = pool.stats();
43
43
  while (idleWorkers !== totalWorkers && !force) {
44
44
  await setTimeoutPromise(100); // Wait a bit workers to finish and try again
45
+
46
+ if (!pool) { // pool might have been terminated in the meantime
47
+ return;
48
+ }
45
49
  ({idleWorkers, totalWorkers} = pool.stats());
46
50
  }
47
51
 
@@ -222,7 +226,7 @@ export default async function({
222
226
  const sourceMapJson = JSON.parse(sourceMapContent);
223
227
 
224
228
  if (sourceMapJson.sections) {
225
- // TODO 4.0
229
+ // TODO 5.0
226
230
  // Module "@jridgewell/trace-mapping" (used by Terser) can't handle index map sections lacking
227
231
  // a "names" array. Since this is a common occurrence for UI5 Tooling bundles, we search for
228
232
  // such cases here and fix them until https://github.com/jridgewell/trace-mapping/pull/29 is
@@ -37,6 +37,10 @@ function getPool(taskUtil) {
37
37
  let {idleWorkers, totalWorkers} = pool.stats();
38
38
  while (idleWorkers !== totalWorkers && !force) {
39
39
  await setTimeoutPromise(100); // Wait a bit workers to finish and try again
40
+
41
+ if (!pool) { // pool might have been terminated in the meantime
42
+ return;
43
+ }
40
44
  ({idleWorkers, totalWorkers} = pool.stats());
41
45
  }
42
46
 
@@ -1,4 +1,5 @@
1
1
  import moduleBundler from "../../processors/bundlers/moduleBundler.js";
2
+ import {applyDefaultsToBundleDefinition} from "./utils/applyDefaultsToBundleDefinition.js";
2
3
  import createModuleNameMapping from "./utils/createModuleNameMapping.js";
3
4
  import ReaderCollectionPrioritized from "@ui5/fs/ReaderCollectionPrioritized";
4
5
 
@@ -94,12 +95,20 @@ export default async function({
94
95
  }
95
96
  });
96
97
  }
97
-
98
+ const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
99
+ const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
98
100
  return combo.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}").then((resources) => {
99
- const options = {bundleDefinition, bundleOptions};
101
+ const options = {
102
+ bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil),
103
+ bundleOptions,
104
+ allowStringBundling
105
+ };
100
106
  if (!optimize && taskUtil) {
101
107
  options.moduleNameMapping = createModuleNameMapping({resources, taskUtil});
102
108
  }
109
+ if (coreVersion) {
110
+ options.targetUi5CoreVersion = coreVersion;
111
+ }
103
112
  return moduleBundler({options, resources}).then((bundles) => {
104
113
  return Promise.all(bundles.map(({bundle, sourceMap} = {}) => {
105
114
  if (!bundle) {
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import moduleBundler from "../../processors/bundlers/moduleBundler.js";
3
+ import {applyDefaultsToBundleDefinition} from "./utils/applyDefaultsToBundleDefinition.js";
3
4
  import {getLogger} from "@ui5/logger";
4
5
  const log = getLogger("builder:tasks:bundlers:generateComponentPreload");
5
6
  import {negateFilters} from "../../lbt/resources/ResourceFilterList.js";
@@ -144,18 +145,24 @@ export default async function({
144
145
  );
145
146
  });
146
147
  }
147
-
148
+ const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
149
+ const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
148
150
  return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => {
149
151
  log.verbose(`Generating ${bundleDefinition.name}...`);
152
+ const options = {
153
+ bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil),
154
+ bundleOptions: {
155
+ ignoreMissingModules: true,
156
+ optimize: true
157
+ },
158
+ allowStringBundling
159
+ };
160
+ if (coreVersion) {
161
+ options.targetUi5CoreVersion = coreVersion;
162
+ }
150
163
  return moduleBundler({
151
164
  resources,
152
- options: {
153
- bundleDefinition,
154
- bundleOptions: {
155
- ignoreMissingModules: true,
156
- optimize: true
157
- }
158
- }
165
+ options
159
166
  });
160
167
  }));
161
168
  })
@@ -29,8 +29,7 @@ import semver from "semver";
29
29
  * @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
30
30
  */
31
31
  export default async function({workspace, taskUtil, options = {}}) {
32
- // Backward compatibility: "namespace" option got renamed to "projectNamespace"
33
- const namespace = options.projectNamespace || options.namespace;
32
+ const namespace = options.projectNamespace;
34
33
 
35
34
  // Use the given namespace if available, otherwise use no namespace
36
35
  // (e.g. in case no manifest.json is present)