Skip to content

Commit

Permalink
[FEATURE] bundle/Builder: Support async require sections and conditio…
Browse files Browse the repository at this point in the history
…nal core boot

The builder is able to create bundles not containing "sap.ui.requireSync" calls, so that the bundles can be used also within UI5 Version 2 and later, where this internal API is not available anymore.

JIRA: CPOUI5FOUNDATION-789

Co-authored-by: Merlin Beutlberger <[email protected]>
  • Loading branch information
RandomByte authored and flovogt committed Jun 12, 2024
1 parent 1cdae55 commit e421e2f
Show file tree
Hide file tree
Showing 21 changed files with 1,328 additions and 169 deletions.
130 changes: 71 additions & 59 deletions lib/lbt/bundle/Builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import BundleSplitter from "./AutoSplitter.js";
import {SectionType} from "./BundleDefinition.js";
import BundleWriter from "./BundleWriter.js";
import {getLogger} from "@ui5/logger";
import semver from "semver";
const log = getLogger("lbt:bundle:Builder");

const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/;
Expand All @@ -40,54 +41,68 @@ function isEmptyBundle(resolvedBundle) {
return resolvedBundle.sections.every((section) => section.modules.length === 0);
}

const EVOBundleFormat = {
beforePreloads(section) {
return `sap.ui.require.preload({\n`;
},
class BundleBuilder {
constructor(pool, targetUi5CoreVersion) {
this.pool = pool;
this.resolver = new BundleResolver(pool);
this.splitter = new BundleSplitter(pool, this.resolver);
this.targetUi5CoreVersion = targetUi5CoreVersion;
this.targetUi5CoreVersionMajor = undefined;
}

getUi5MajorVersion() {
if (this.targetUi5CoreVersionMajor !== undefined) {
return this.targetUi5CoreVersionMajor;
}

if (this.targetUi5CoreVersion && semver.valid(this.targetUi5CoreVersion)) {
this.targetUi5CoreVersionMajor = semver.major(this.targetUi5CoreVersion);
} else {
// Assume legacy version if unable to determine the version
this.targetUi5CoreVersionMajor === null;
}
return this.targetUi5CoreVersionMajor;
}

afterPreloads(section) {
generateAfterPreloads(section) {
let str = `}`;
if ( section.name ) {
str += `,"${section.name}"`;
}
str += `);\n`;
return str;
},
}

beforeBundleInfo() {
return "sap.ui.loader.config({bundlesUI5:{\n";
},
generateRequire(modules) {
const requireCallback = this.determineRequireCallback(modules) ?? "";
return `sap.ui.require([${
modules.map(($) => `"${toRequireJSName($)}"`).join(",\n")
}]${requireCallback});\n`;
}

afterBundleInfo() {
return "}});\n";
},
determineRequireCallback(modules) {
if (this.getUi5MajorVersion() >= 2) {
// Starting with UI5 2.0.0, method Core.boot does not exist anymore
return;
}
const coreModuleIndex = modules.indexOf(MODULE__SAP_UI_CORE_CORE);
if (coreModuleIndex === -1) {
return;
}
return `, (${
modules.map((m, i) => i === coreModuleIndex ? `Core` : `_m${i}`)
}) => Core.boot?.()`;
}

requireSync(moduleName) {
generateRequireSync(moduleName) {
return `sap.ui.requireSync("${toRequireJSName(moduleName)}");\n`;
},
}

shouldDecorate(resolvedModule) {
executesLoaderOrCore(resolvedModule) {
return resolvedModule.executes(MODULE__UI5LOADER) ||
resolvedModule.executes(MODULE__UI5LOADER_AUTOCONFIG) ||
resolvedModule.executes(MODULE__JQUERY_SAP_GLOBAL) ||
resolvedModule.executes(MODULE__SAP_UI_CORE_CORE);
},

beforeDepCache(outW) {
outW.writeln(`sap.ui.loader.config({depCacheUI5:{`);
},

afterDepCache(outW) {
outW.writeln(`}});`);
}
};

class BundleBuilder {
constructor(pool) {
this.pool = pool;
this.resolver = new BundleResolver(pool);
this.splitter = new BundleSplitter(pool, this.resolver);
this.targetBundleFormat = null;
}

async createBundle(module, options) {
Expand Down Expand Up @@ -117,13 +132,10 @@ class BundleBuilder {
this.options.sourceMap = true;
}

// Since UI5 Tooling 3.0: Always use modern API
this.targetBundleFormat = EVOBundleFormat;

// when decorateBootstrapModule is false,
// we don't write the optimized flag and don't write the try catch wrapper
this.shouldDecorate = this.options.decorateBootstrapModule &&
this.targetBundleFormat.shouldDecorate(resolvedModule);
this.executesLoaderOrCore(resolvedModule);
// TODO is the following condition ok or should the availability of jquery.sap.global.js be configurable?
this.jqglobalAvailable = !resolvedModule.containsGlobal;
this.openModule(resolvedModule.name);
Expand Down Expand Up @@ -195,11 +207,14 @@ class BundleBuilder {
}

closeModule(resolvedModule) {
if ( resolvedModule.containsCore ) {
if ( resolvedModule.containsCoreSync ) {
if ( this.getUi5MajorVersion() >= 2 ) {
throw new Error("Requiring sap/ui/core/Core synchronously is not supported as of UI5 Version 2");
}
this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
this.writeWithSourceMap(
`// as this module contains the Core, we ensure that the Core has been booted\n` +
`sap.ui.getCore().boot && sap.ui.getCore().boot();`);
`sap.ui.getCore?.().boot?.();`);
}
if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) {
this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
Expand Down Expand Up @@ -303,7 +318,7 @@ class BundleBuilder {

await this.rewriteAMDModules(sequence);
if ( sequence.length > 0 ) {
this.writeWithSourceMap(this.targetBundleFormat.beforePreloads(section));
this.writeWithSourceMap(`sap.ui.require.preload({\n`);
let i = 0;
for ( const module of sequence ) {
const resource = await this.pool.findResourceWithInfo(module);
Expand All @@ -326,10 +341,8 @@ class BundleBuilder {
if ( i > 0 ) {
outW.writeln();
}
outW.write(this.targetBundleFormat.afterPreloads(section));
outW.write(this.generateAfterPreloads(section));
}

// this.afterWriteFunctionPreloadSection();
}

beforeWriteFunctionPreloadSection(sequence) {
Expand Down Expand Up @@ -421,12 +434,6 @@ class BundleBuilder {
Array.prototype.splice.apply(sequence, [0, sequence.length].concat(remaining));
}

afterWriteFunctionPreloadSection() {
}

beforeWritePreloadModule(module, info, resource) {
}

/**
*
* @param {string} moduleName module name
Expand Down Expand Up @@ -533,7 +540,7 @@ class BundleBuilder {

let bundleInfoStr = "";
if ( sections.length > 0 ) {
bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
bundleInfoStr = "sap.ui.loader.config({bundlesUI5:{\n";
sections.forEach((section, idx) => {
if ( idx > 0 ) {
bundleInfoStr += ",\n";
Expand All @@ -549,18 +556,24 @@ class BundleBuilder {
}
bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
});
bundleInfoStr += "\n";
bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
bundleInfoStr += "\n}});\n";

this.writeWithSourceMap(bundleInfoStr);
}
}

writeRequires(section) {
if (section.modules.length === 0) {
return;
}
this.outW.ensureNewLine();
section.modules.forEach( (module) => {
this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
});
if (section.async === false) {
section.modules.forEach( (module) => {
this.writeWithSourceMap(this.generateRequireSync(module));
});
} else {
this.writeWithSourceMap(this.generateRequire(section.modules));
}
}

// When AutoSplit is enabled for depCache, we need to ensure that modules
Expand All @@ -569,7 +582,6 @@ class BundleBuilder {
// module in the next file. This will also duplicate its dependency definition if we do not filter.
#depCacheSet = new Set();
async writeDepCache(section) {
const outW = this.outW;
let hasDepCache = false;

const sequence = section.modules.slice().sort();
Expand Down Expand Up @@ -597,11 +609,11 @@ class BundleBuilder {
if (deps.length > 0) {
if (!hasDepCache) {
hasDepCache = true;
outW.ensureNewLine();
this.targetBundleFormat.beforeDepCache(outW, section);
this.outW.ensureNewLine();
this.outW.writeln(`sap.ui.loader.config({depCacheUI5:{`);
}

outW.writeln(
this.outW.writeln(
`"${module}": [${deps.map((dep) => `"${dep}"`).join(",")}],`
);
} else {
Expand All @@ -611,7 +623,7 @@ class BundleBuilder {
}

if (hasDepCache) {
this.targetBundleFormat.afterDepCache(outW, section);
this.outW.writeln(`}});`);
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions lib/lbt/bundle/ResolvedBundleDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class ResolvedBundleDefinition {
);
}

get containsCore() {
get containsCoreSync() {
return this.sections.some(
(section) =>
(section.mode === SectionType.Raw || section.mode === SectionType.Require) &&
(section.mode === SectionType.Raw ||
(section.mode === SectionType.Require && section.async === false)) &&
section.modules.some((module) => module === MODULE__SAP_UI_CORE_CORE)
);
}
Expand Down Expand Up @@ -109,6 +110,10 @@ class ResolvedSection {
get declareRawModules() {
return this.sectionDefinition.declareRawModules;
}

get async() {
return this.sectionDefinition.async;
}
}

export default ResolvedBundleDefinition;
9 changes: 7 additions & 2 deletions lib/processors/bundlers/moduleBundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,16 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
bundle definition
* @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [parameters.options.bundleOptions] Module
bundle options
* @param {string} [parameters.options.targetUi5CoreVersion] Optional semver compliant sap.ui.core project version, e.g '2.0.0'.
This allows the bundler to make assumptions on available runtime APIs.
Omit if the ultimate UI5 version at runtime is unknown or can't be determined.
* @returns {Promise<module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundlerResult[]>}
* Promise resolving with module bundle resources
*/
/* eslint-enable max-len */
export default function({resources, options: {bundleDefinition, bundleOptions, moduleNameMapping}}) {
export default function({resources, options: {
bundleDefinition, bundleOptions, moduleNameMapping, targetUi5CoreVersion
}}) {
// Apply defaults without modifying the passed object
bundleOptions = Object.assign({}, {
optimize: true,
Expand All @@ -150,7 +155,7 @@ export default function({resources, options: {bundleDefinition, bundleOptions, m
const pool = new LocatorResourcePool({
ignoreMissingModules: bundleOptions.ignoreMissingModules
});
const builder = new BundleBuilder(pool);
const builder = new BundleBuilder(pool, targetUi5CoreVersion);

if (log.isLevelEnabled("verbose")) {
log.verbose(`Generating bundle:`);
Expand Down
5 changes: 4 additions & 1 deletion lib/tasks/bundlers/generateBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,15 @@ export default async function({
}
});
}

const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
return combo.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}").then((resources) => {
const options = {bundleDefinition, bundleOptions};
if (!optimize && taskUtil) {
options.moduleNameMapping = createModuleNameMapping({resources, taskUtil});
}
if (coreVersion) {
options.targetUi5CoreVersion = coreVersion;
}
return moduleBundler({options, resources}).then((bundles) => {
return Promise.all(bundles.map(({bundle, sourceMap} = {}) => {
if (!bundle) {
Expand Down
20 changes: 12 additions & 8 deletions lib/tasks/bundlers/generateComponentPreload.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,22 @@ export default async function({
);
});
}

const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => {
log.verbose(`Generating ${bundleDefinition.name}...`);
const options = {
bundleDefinition,
bundleOptions: {
ignoreMissingModules: true,
optimize: true
}
};
if (coreVersion) {
options.targetUi5CoreVersion = coreVersion;
}
return moduleBundler({
resources,
options: {
bundleDefinition,
bundleOptions: {
ignoreMissingModules: true,
optimize: true
}
}
options
});
}));
})
Expand Down
8 changes: 6 additions & 2 deletions lib/tasks/bundlers/generateLibraryPreload.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ function getDefaultLibraryPreloadFilters(namespace, excludes) {
function getBundleDefinition(namespace, excludes) {
// Note: This configuration is only used when no bundle definition in ui5.yaml exists (see "skipBundles" parameter)

// TODO: Remove this hardcoded bundle definition.
// TODO: Remove this hardcoded bundle definition once support for relevant versions has ended.
// sap.ui.core ui5.yaml contains a configuration since UI5 1.103.0 (specVersion 2.4)
// so this is still required to build UI5 versions <= 1.102.0.
// so this is still required to build UI5 versions <= 1.102.0 (such as 1.84 and 1.96)
if (namespace === "sap/ui/core") {
return {
name: `${namespace}/library-preload.js`,
Expand Down Expand Up @@ -255,12 +255,16 @@ export default async function({workspace, taskUtil, options: {skipBundles = [],
}
});
}
const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();

const execModuleBundlerIfNeeded = ({options, resources}) => {
if (skipBundles.includes(options.bundleDefinition.name)) {
log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`);
return null;
}
if (coreVersion) {
options.targetUi5CoreVersion = coreVersion;
}
return moduleBundler({options, resources});
};

Expand Down
Loading

0 comments on commit e421e2f

Please sign in to comment.