@ui5/builder
Version:
UI5 Tooling - Builder
593 lines (517 loc) • 18.8 kB
JavaScript
/* eslint quotes: ["error", "double", { "allowTemplateLiterals": true }] */
// for consistency of write calls, we generally allow template literals
;
const uglify = require("uglify-es");
const {pd} = require("pretty-data");
const esprima = require("esprima");
const escodegen = require("escodegen");
const {Syntax} = esprima;
// const MOZ_SourceMap = require("source-map");
const {isMethodCall} = require("../utils/ASTUtils");
const ModuleName = require("../utils/ModuleName");
const UI5ClientConstants = require("../UI5ClientConstants");
const BundleResolver = require("./Resolver");
const BundleSplitter = require("./AutoSplitter");
const {SectionType} = require("./BundleDefinition");
const BundleWriter = require("./BundleWriter");
const log = require("@ui5/logger").getLogger("lbt:bundle:Builder");
const copyrightCommentsPattern = /copyright|\(c\)|released under|license|\u00a9/i;
const xmlHtmlPrePattern = /<(?:\w+:)?pre>/;
const strReplacements = {
"\r": "\\r",
"\t": "\\t",
"\n": "\\n",
"'": "\\'",
"\\": "\\\\"
};
function makeStringLiteral(str) {
return "'" + String(str).replace(/['\r\n\t\\]/g, function(char) {
return strReplacements[char];
}) + "'";
}
const UI5BundleFormat = {
beforePreloads(outW, section) {
outW.write(`jQuery.sap.registerPreloadedModules(`);
outW.writeln(`{`);
if ( section.name ) {
outW.writeln(`"name":"${section.getSectionName()}",`);
}
outW.writeln(`"version":"2.0",`);
outW.writeln(`"modules":{`);
},
afterPreloads(outW, section) {
outW.writeln(`}});`);
},
requireSync(outW, moduleName) {
outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
},
shouldDecorate(resolvedModule) {
return resolvedModule.executes(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL);
},
supportsNativeDefine() {
return false;
}
};
const EVOBundleFormat = {
beforePreloads(outW, section) {
outW.writeln(`sap.ui.require.preload({`);
},
afterPreloads(outW, section) {
outW.write(`}`);
if ( section.name ) {
outW.write(`,"${section.getSectionName()}"`);
}
outW.writeln(`);`);
},
requireSync(outW, moduleName) {
outW.writeln(`sap.ui.requireSync("${ModuleName.toRequireJSName(moduleName)}");`);
},
shouldDecorate(resolvedModule) {
return resolvedModule.executes(UI5ClientConstants.MODULE__UI5LOADER) ||
resolvedModule.executes(UI5ClientConstants.MODULE__UI5LOADER_AUTOCONFIG) ||
resolvedModule.executes(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL) ||
resolvedModule.executes(UI5ClientConstants.MODULE__SAP_UI_CORE_CORE);
},
supportsNativeDefine() {
// disabled in 1.54 as the standard global names define/require are not written by the ui5loader
return false;
}
};
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) {
await this._prepare();
if ( options.numberOfParts > 1 ) {
const bundleInfos = [];
const submodules = await this.splitter.run( module, options );
for ( const submodule of submodules ) {
bundleInfos.push( await this._createBundle(submodule, options) );
}
return bundleInfos;
} else {
return this._createBundle(module, options);
}
}
_prepare() {
return Promise.all([
// check whether the resource pool contains debug and optimized sources
this.pool.findResource( ModuleName.getDebugName(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL) ).
then( () => this.optimizedSources = true, () => this.optimizedSources = false ),
// check whether EVO modules are available. If so, use EVO APIs, else use old UI5 APIs.
this.pool.findResource(UI5ClientConstants.EVO_MARKER_RESOURCE).
then( () => this.targetBundleFormat = EVOBundleFormat, () => this.targetBundleFormat = UI5BundleFormat )
]);
}
async _createBundle(module, options) {
const resolvedModule = await this.resolver.resolve(module /* NODE-TODO, vars */);
log.verbose(" create '%s'", resolvedModule.name);
this.options = options || {};
this.optimize = !!this.options.optimize;
// when decorateBootstrapModule is set to false, we don't write the optimized flag
// and don't write the try catch wrapper
this.shouldDecorate = this.options.decorateBootstrapModule &&
(((this.optimizedSources && !this.options.debugMode) || this.optimize) &&
this.targetBundleFormat.shouldDecorate(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);
this.writeConfiguration(resolvedModule.configuration); // NODE-TODO configuration currently will be undefined
// create all sections in sequence
for ( const section of resolvedModule.sections ) {
log.verbose(" adding section%s of type %s",
section.name ? " '" + section.name + "'" : "", section.mode);
await this.addSection(section);
}
this.closeModule(resolvedModule);
const bundleInfo = await resolvedModule.createModuleInfo(this.pool);
bundleInfo.size = this.outW.length;
return {
name: module.name,
content: this.outW.toString(),
bundleInfo: bundleInfo
};
}
openModule(module) {
this.outW = new BundleWriter();
this.missingRawDeclarations = [];
if ( this.shouldDecorate ) {
this.outW.writeln(`window["sap-ui-optimized"] = true;`);
if ( this.options.addTryCatchRestartWrapper ) {
this.outW.writeln(`try {`);
}
}
}
closeModule(resolvedModule) {
if ( resolvedModule.containsCore ) {
this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
this.outW.writeln(`// as this module contains the Core, we ensure that the Core has been booted`);
this.outW.writeln(`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
this.outW.writeln(`} catch(oError) {`);
this.outW.writeln(`if (oError.name != "Restart") { throw oError; }`);
this.outW.writeln(`}`);
}
/* NODE-TODO
if ( writeSourceMap && writeSourceMapAnnotation ) {
outW.ensureNewLine();
outW.write("//# sourceMappingURL=" + moduleName.getBaseName().replaceFirst("\\.js$", ".js.map"));
}*/
}
addSection(section) {
this.ensureRawDeclarations();
switch (section.mode) {
case SectionType.Provided:
// do nothing
return undefined; // nothing to wait for
case SectionType.Raw:
return this.writeRaw(section);
case SectionType.Preload:
return this.writePreloadFunction(section);
case SectionType.Require:
return this.writeRequires(section);
default:
throw new Error("unknown section mode " + section.mode);
}
}
ensureRawDeclarations() {
if ( this.missingRawDeclarations.length && this.jqglobalAvailable ) {
this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
/* NODE-TODO, moduleName is not defined
It should contain the name of the module which is currently build (1st parameter of _createBundle).
But when the ui5loader is present, declareRawModules should be forced to false anyhow.
this.outW.writeln("jQuery.sap.declare('", ModuleName.toUI5LegacyName(moduleName), "');");
*/
this.missingRawDeclarations.forEach( (module) => {
// 2nd parameter set to 'false': do not create namespaces - they nevertheless would come too late
this.outW.writeln(`jQuery.sap.declare('${ModuleName.toUI5LegacyName(module)}', false);`);
});
this.missingRawDeclarations = [];
}
}
writeConfiguration(config) {
if ( !config ) {
return;
}
const outW = this.outW;
outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
outW.writeln(`(function(window){`);
outW.writeln(`\tvar cfg=window['sap-ui-config']=window['sap-ui-config']||{},`);
outW.writeln(`\t\troots=cfg.resourceRoots=cfg.resourceRoots||{};`);
config.propertyName.forEach( (property) => {
outW.writeln(`\tcfg[${makeStringLiteral(property)}]=${config.getPropertyAsJSLiteral(property)};`);
});
Object.keys(config.resourceRoots).forEach( (prefix) => {
outW.writeln(`\troots[${makeStringLiteral(prefix)}]=${makeStringLiteral(config.resourceRoots[prefix])};`);
});
outW.writeln(`}(window));`);
}
// TODO check that there are only JS modules contained
async writeRaw(section) {
// write all modules in sequence
for ( const module of section.modules ) {
const resource = await this.pool.findResourceWithInfo(module);
if ( resource != null ) {
this.outW.startSegment(module);
await this.writeRawModule(module, resource);
const compressedSize = this.outW.endSegment();
log.verbose(" %s (%d,%d)", module, resource.info != null ? resource.info.size : -1, compressedSize);
if ( section.declareRawModules ) {
this.missingRawDeclarations.push(module);
}
if ( module === UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL ) {
this.jqglobalAvailable = true;
}
} else {
log.error(" couldn't find %s", module);
}
}
}
async writeRawModule(module, resource) {
let fileContent = await resource.buffer();
if ( /\.js$/.test(module) ) {
fileContent = this.compressJS( fileContent, resource );
}
this.outW.ensureNewLine();
this.outW.write( fileContent );
}
async writePreloadFunction(section) {
const outW = this.outW;
const avoidLazyParsing = section.sectionDefinition.avoidLazyParsing;
outW.ensureNewLine();
const sequence = section.modules.slice();
this.beforeWriteFunctionPreloadSection(sequence);
await this.rewriteAMDModules(sequence, avoidLazyParsing);
if ( sequence.length > 0 ) {
this.targetBundleFormat.beforePreloads(outW, section);
let i = 0;
for ( const module of sequence ) {
const resource = await this.pool.findResourceWithInfo(module);
if ( resource != null ) {
if ( i>0 ) {
outW.writeln(",");
}
this.beforeWritePreloadModule(module, resource.info, resource);
outW.write(`\t"${module.toString()}":`);
outW.startSegment(module);
await this.writePreloadModule(module, resource.info, resource, avoidLazyParsing);
const compressedSize = outW.endSegment();
log.verbose(" %s (%d,%d)", module,
resource.info != null ? resource.info.size : -1, compressedSize);
i++;
} else {
log.error(" couldn't find %s", module);
}
}
if ( i > 0 ) {
outW.writeln();
}
this.targetBundleFormat.afterPreloads(outW, section);
}
// this.afterWriteFunctionPreloadSection();
}
compressJS(fileContent, resource) {
if ( this.optimize ) {
const result = uglify.minify({
[resource.name]: String(fileContent)
}, {
warnings: false, // TODO configure?
compress: false, // TODO configure?
output: {
comments: copyrightCommentsPattern
}
// , outFileName: resource.name
// , outSourceMap: true
});
if ( result.error ) {
throw result.error;
}
// console.log(result.map);
// const map = new MOZ_SourceMap.SourceMapConsumer(result.map);
// map.eachMapping(function (m) { console.log(m); }); // console.log(map);
fileContent = result.code;
// throw new Error();
}
return fileContent;
}
async compressJSAsync(resource) {
const content = await resource.buffer();
return this.compressJS( content, resource );
}
beforeWriteFunctionPreloadSection(sequence) {
// simple version: just sort alphabetically
sequence.sort();
}
async rewriteAMDModules(sequence, avoidLazyParsing) {
if ( this.options.usePredefineCalls ) {
const outW = this.outW;
const remaining = [];
for ( const module of sequence ) {
if ( /\.js$/.test(module) ) {
// console.log("Processing " + module);
const resource = await this.pool.findResourceWithInfo(module);
const code = await resource.buffer();
const ast = rewriteDefine(this.targetBundleFormat, code, module, avoidLazyParsing);
if ( ast ) {
outW.startSegment(module);
outW.ensureNewLine();
const astAsCode = escodegen.generate(ast);
const fileContent = this.compressJS(astAsCode, resource);
outW.write( fileContent );
outW.ensureNewLine();
const compressedSize = outW.endSegment();
log.verbose(" %s (%d,%d)", module,
resource.info != null ? resource.info.size : -1, compressedSize);
} else {
// keep unprocessed modules
remaining.push(module);
}
} else {
// keep unprocessed modules
remaining.push(module);
}
}
Array.prototype.splice.apply(sequence, [0, sequence.length].concat(remaining));
}
}
afterWriteFunctionPreloadSection() {
}
beforeWritePreloadModule(module, info, resource) {
}
async writePreloadModule(module, info, resource, avoidLazyParsing) {
const outW = this.outW;
if ( /\.js$/.test(module) && (info == null || !info.requiresTopLevelScope) ) {
const compressedContent = await this.compressJSAsync( resource );
if ( avoidLazyParsing ) {
outW.write(`(`);
}
outW.write(`function(){`);
outW.write( compressedContent );
this.exportGlobalNames(info);
outW.ensureNewLine();
outW.write(`}`);
if ( avoidLazyParsing ) {
outW.write(`)`);
}
} else if ( /\.js$/.test(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
log.warn("**** warning: module %s requires top level scope" +
" and can only be embedded as a string (requires 'eval')", module);
const compressedContent = await this.compressJSAsync( resource );
outW.write( makeStringLiteral( compressedContent ) );
} else if ( /\.html$/.test(module) ) {
const fileContent = await resource.buffer();
outW.write( makeStringLiteral( fileContent ) );
} else if ( /\.json$/.test(module) ) {
let fileContent = await resource.buffer();
if ( this.optimize ) {
try {
fileContent = JSON.stringify( JSON.parse( fileContent) );
} catch (e) {
log.error(e);
}
}
outW.write(makeStringLiteral(fileContent));
} else if ( /\.xml$/.test(module) ) {
let fileContent = await resource.buffer();
if ( this.optimize ) {
// For XML we use the pretty data
// Do not minify if XML(View) contains an <*:pre> tag,
// because whitespace of HTML <pre> should be preserved (should only happen rarely)
if (!xmlHtmlPrePattern.test(fileContent)) {
fileContent = pd.xmlmin(fileContent.toString(), false);
}
}
outW.write( makeStringLiteral( fileContent ) );
} else if ( /\.properties$/.test(module) ) {
// same as for other text files, but input encoding is ISO_8859_1
const fileContent = await resource.buffer();
outW.write( makeStringLiteral( fileContent.toString("latin1") ) );
} else {
log.error("don't know how to embed module " + module); // TODO throw?
}
return true;
}
exportGlobalNames(info) {
if ( !info || !info.exposedGlobalNames || !info.exposedGlobalNames.length ) {
return;
}
this.outW.ensureNewLine();
info.exposedGlobalNames.forEach( (globalName) => {
this.outW.writeln(`this.${globalName}=${globalName};`);
});
}
writeRequires(section) {
this.outW.ensureNewLine();
section.modules.forEach( (module) => {
this.targetBundleFormat.requireSync(this.outW, module);
});
}
}
const CALL_DEFINE = ["define"];
const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
function rewriteDefine(targetBundleFormat, code, moduleName, avoidLazyParsing) {
function _injectModuleNameIfNeeded(defineCall) {
if ( defineCall.arguments.length == 0
|| defineCall.arguments[0].type !== Syntax.Literal ) {
defineCall.arguments.unshift({
type: Syntax.Literal,
value: ModuleName.toRequireJSName(moduleName)
});
}
}
function enforceEagerParsing(defineCall) {
const args = defineCall.arguments;
// wrap factory function to avoid lazy parsing (V8 and Chakra)
let iFactory = 0;
// skip any hard coded module name
if ( iFactory < args.length && args[iFactory].type === Syntax.Literal ) {
iFactory++;
}
// skip array of dependencies
if ( iFactory < args.length && args[iFactory].type === Syntax.ArrayExpression ) {
iFactory++;
}
// wrap factory
if ( iFactory < args.length && args[iFactory].type === Syntax.FunctionExpression ) {
/* NODE-TODO
Token firstToken = doc.getToken(args.getChild(iFactory).getTokenStartIndex());
Token lastToken = doc.getToken(args.getChild(iFactory).getTokenStopIndex());
if ( "function".equals(firstToken.getText())
&& "}".equals(lastToken.getText()) ) {
firstToken.setText("(" + firstToken.getText());
lastToken.setText(lastToken.getText() + ")");
} */
}
}
let ast;
try {
ast = esprima.parseScript(code.toString(), {loc: true});
} catch (e) {
log.error("error while parsing %s:%s", module, e);
return;
}
if ( ast.type === Syntax.Program
&& ast.body.length === 1
&& ast.body[0].type === Syntax.ExpressionStatement ) {
// rewrite define to require.predefine
if ( targetBundleFormat.supportsNativeDefine() && isMethodCall(ast.body[0].expression, CALL_DEFINE) ) {
const defineCall = ast.body[0].expression;
if ( defineCall.callee.type === Syntax.Identifier
&& defineCall.callee.name === "define" ) {
defineCall.callee.name = "predefine"; // rename method to predefine
// add a member expression 'sap.ui.require.'
defineCall.callee = {
type: Syntax.MemberExpression,
object: {
type: Syntax.MemberExpression,
object: {
type: Syntax.MemberExpression,
object: {
type: Syntax.Identifier,
name: "sap"
},
property: {
type: Syntax.Identifier,
name: "ui"
}
},
property: {
type: Syntax.Identifier,
name: "require"
}
},
property: defineCall.callee
};
}
_injectModuleNameIfNeeded(defineCall, moduleName);
if ( avoidLazyParsing ) {
enforceEagerParsing(defineCall);
}
// console.log("rewriting %s", module, defineCall);
return ast;
}
if ( isMethodCall(ast.body[0].expression, CALL_SAP_UI_DEFINE) ) {
const defineCall = ast.body[0].expression;
// rewrite sap.ui.define to sap.ui.predefine
if ( defineCall.callee.type === Syntax.MemberExpression
&& defineCall.callee.property.type === Syntax.Identifier
&& defineCall.callee.property.name === "define" ) {
defineCall.callee.property.name = "predefine";
}
_injectModuleNameIfNeeded(defineCall, moduleName);
if ( avoidLazyParsing ) {
enforceEagerParsing(defineCall);
}
// console.log("rewriting %s", module, defineCall);
return ast;
}
}
return false;
}
module.exports = BundleBuilder;