UNPKG

@ui5/builder

Version:
911 lines (811 loc) 30.5 kB
/* eslint quotes: ["error", "double", { "allowTemplateLiterals": true }] */ // for consistency of write calls, we generally allow template literals import path from "node:path"; import {pd} from "pretty-data"; import {parseJS, Syntax} from "../utils/parseUtils.js"; import {encode as encodeMappings, decode as decodeMappings} from "@jridgewell/sourcemap-codec"; import {isMethodCall} from "../utils/ASTUtils.js"; import {toUI5LegacyName, toRequireJSName} from "../utils/ModuleName.js"; import { MODULE__UI5LOADER, MODULE__UI5LOADER_AUTOCONFIG, MODULE__JQUERY_SAP_GLOBAL, MODULE__SAP_UI_CORE_CORE} from "../UI5ClientConstants.js"; import escapePropertiesFile from "../utils/escapePropertiesFile.js"; import {makeStringLiteral, removeHashbang} from "../utils/stringUtils.js"; import BundleResolver from "./Resolver.js"; 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*$/; const httpPattern = /^https?:\/\//i; const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/; function isEmptyBundle(resolvedBundle) { return resolvedBundle.sections.every((section) => section.modules.length === 0); } class BundleBuilder { constructor(pool, targetUi5CoreVersion, allowStringBundling) { this.pool = pool; this.resolver = new BundleResolver(pool); this.splitter = new BundleSplitter(pool, this.resolver); this.targetUi5CoreVersion = targetUi5CoreVersion; this.targetUi5CoreVersionMajor = undefined; this.allowStringBundling = allowStringBundling; } getEffectiveUi5MajorVersion() { if (this.targetUi5CoreVersionMajor !== undefined) { return this.targetUi5CoreVersionMajor; } const parsedVersion = semver.parse(this.targetUi5CoreVersion); if (parsedVersion) { this.targetUi5CoreVersionMajor = parsedVersion.major; // legacy-free versions include changes of the upcoming major version // so we should treat them the same as the next major version if ( parsedVersion.prerelease.includes("legacy-free") || parsedVersion.prerelease.includes("legacy-free-SNAPSHOT") // Maven snapshot version ) { this.targetUi5CoreVersionMajor += 1; } } else { // Assume legacy version if unable to determine the version this.targetUi5CoreVersionMajor = null; } return this.targetUi5CoreVersionMajor; } generateAfterPreloads(section) { let str = `}`; if ( section.name ) { str += `,"${section.name}"`; } str += `);\n`; return str; } generateRequire(modules) { const requireCallback = this.determineRequireCallback(modules) ?? ""; return `sap.ui.require([${ modules.map(($) => `"${toRequireJSName($)}"`).join(",\n") }]${requireCallback});\n`; } determineRequireCallback(modules) { if (this.getEffectiveUi5MajorVersion() >= 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?.()`; } generateRequireSync(moduleName) { return `sap.ui.requireSync("${toRequireJSName(moduleName)}");\n`; } 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); } async createBundle(module, options) { 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); } } async _createBundle(module, options) { const resolvedModule = await this.resolver.resolve(module); if ( options.skipIfEmpty && isEmptyBundle(resolvedModule) ) { log.verbose(" Skipping empty bundle " + module.name); return undefined; } log.verbose(` Create '${resolvedModule.name}'`); this.options = options || {}; this.optimize = !!this.options.optimize; if (this.options.sourceMap === undefined) { this.options.sourceMap = true; } // 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.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); this._sourceMap = { version: 3, file: path.posix.basename(resolvedModule.name), sections: [], }; this._bundleName = resolvedModule.name; this._inlineContentCounter = 0; let bundleInfos = []; // create all sections in sequence for ( const section of resolvedModule.sections ) { log.verbose(` Adding section${section.name ? " '" + section.name + "'" : ""} of type ${section.mode}`); if ( section.mode === SectionType.BundleInfo ) { bundleInfos.push(section); } else { if ( bundleInfos.length > 0 ) { await this.writeBundleInfos(bundleInfos); bundleInfos = []; } await this.addSection(section); } } if ( bundleInfos.length > 0 ) { await this.writeBundleInfos(bundleInfos); bundleInfos = []; } this.closeModule(resolvedModule); const bundleInfo = await resolvedModule.createModuleInfo(this.pool, this.allowStringBundling); bundleInfo.size = this.outW.length; return { name: module.name, content: this.outW.toString(), sourceMap: this.options.sourceMap ? JSON.stringify(this._sourceMap) : null, bundleInfo: bundleInfo }; } openModule(module) { this.outW = new BundleWriter(); this.missingRawDeclarations = []; this.outW.writeln("//@ui5-bundle " + module); if ( this.shouldDecorate ) { this.outW.writeln(`window["sap-ui-optimized"] = true;`); if ( this.options.addTryCatchRestartWrapper ) { this.outW.writeln(`try {`); } } } writeWithSourceMap(content) { const transientSourceName = `${path.posix.basename(this._bundleName)}?bundle-code-${this._inlineContentCounter++}`; const sourceMap = createTransientSourceMap({ moduleName: transientSourceName, moduleContent: content, includeContent: true }); this.addSourceMap(this._bundleName, sourceMap); this.outW.write(content); this.outW.ensureNewLine(); } closeModule(resolvedModule) { if ( resolvedModule.containsCoreSync ) { if ( this.getEffectiveUi5MajorVersion() >= 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?.();`); } if ( this.shouldDecorate && this.options.addTryCatchRestartWrapper ) { this.outW.ensureNewLine(); // for clarity and to avoid issues with single line comments this.writeWithSourceMap( `} catch(oError) {\n` + `if (oError.name != "Restart") { throw oError; }\n` + `}`); } if (this.options.sourceMap) { this.outW.writeln(`//# sourceMappingURL=${path.posix.basename(resolvedModule.name)}.map`); } } async addSection(section) { this.ensureRawDeclarations(); switch (section.mode) { case SectionType.Provided: // do nothing return undefined; // nothing to wait for case SectionType.Raw: return await this.writeRaw(section); case SectionType.Preload: return await this.writePreloadFunction(section); case SectionType.BundleInfo: return await this.writeBundleInfos([section]); case SectionType.Require: return await this.writeRequires(section); case SectionType.DepCache: return await this.writeDepCache(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('", 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('${toUI5LegacyName(module)}', false);`); }); this.missingRawDeclarations = []; } } // TODO check that there are only JS modules contained async writeRaw(section) { // write all modules in sequence for ( const moduleName of section.modules ) { const resource = await this.pool.findResourceWithInfo(moduleName); if ( resource != null ) { this.outW.startSegment(moduleName); this.outW.ensureNewLine(); this.outW.writeln("//@ui5-bundle-raw-include " + moduleName); await this.writeRawModule(moduleName, resource); const compressedSize = this.outW.endSegment(); log.verbose(` ${moduleName} (${resource.info != null ? resource.info.size : -1},${compressedSize})`); if ( section.declareRawModules ) { this.missingRawDeclarations.push(moduleName); } if ( moduleName === MODULE__JQUERY_SAP_GLOBAL ) { this.jqglobalAvailable = true; } } else { log.error(` Could not find module ${moduleName}`); } } } async writeRawModule(moduleName, resource) { this.outW.ensureNewLine(); let moduleContent = (await resource.buffer()).toString(); moduleContent = removeHashbang(moduleContent); if (this.options.sourceMap) { let moduleSourceMap; ({moduleContent, moduleSourceMap} = await this.getSourceMapForModule({ moduleName, moduleContent, resourcePath: resource.getPath() })); this.addSourceMap(moduleName, moduleSourceMap); } this.outW.write(moduleContent); this.outW.ensureNewLine(); } async writePreloadFunction(section) { const outW = this.outW; outW.ensureNewLine(); const sequence = section.modules.slice(); this.beforeWriteFunctionPreloadSection(sequence); await this.rewriteAMDModules(sequence); if ( sequence.length > 0 ) { this.writeWithSourceMap(`sap.ui.require.preload({\n`); let i = 0; for ( const module of sequence ) { const resource = await this.pool.findResourceWithInfo(module); if ( resource != null ) { if ( i>0 ) { outW.writeln(","); } outW.write(`\t"${module.toString()}":`); outW.startSegment(module); await this.writePreloadModule(module, resource.info, resource); const compressedSize = outW.endSegment(); log.verbose(` ${module} (${resource.info != null ? resource.info.size : -1},${compressedSize})`); i++; } else { log.error(` Could not find module ${module}`); } } if ( i > 0 ) { outW.writeln(); } outW.write(this.generateAfterPreloads(section)); } } beforeWriteFunctionPreloadSection(sequence) { // simple version: just sort alphabetically sequence.sort(); } addSourceMap(moduleName, map) { if (!map) { throw new Error("No source map provided"); } // Reminder on the structure of line-segments in the map: // [generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex] if (map.mappings.startsWith(";")) { // If first line is not already mapped (typical for comments or parentheses), add a mapping to // make sure that dev-tools (especially Chrome's) don't choose the end of the preceding module // when the user tries to set a breakpoint from the bundle file map.mappings = "AAAA" + map.mappings; } else if (this.outW.columnOffset === 0 && !map.mappings.startsWith("A")) { // If first column of the first line is not already mapped, add a mapping for the same reason as above. // This is typical for transpiled code, where there is a bunch of generated code at the beginning that // can't be mapped to the original source if (map.mappings) { map.mappings = "AAAA," + map.mappings; } else { // If there are no existing mappings (e.g. if the file is empty or contains only comments), // do not specify any mapping. Especially Safari would otherwise reject the source map due to // "Invalid Mappings" map.mappings = ""; } } map.sourceRoot = path.posix.relative( path.posix.dirname(this._bundleName), path.posix.dirname(moduleName)); this._sourceMap.sections.push({ offset: { line: this.outW.lineOffset, column: this.outW.columnOffset }, map }); } async rewriteAMDModules(sequence) { const outW = this.outW; const remaining = []; for ( const moduleName of sequence ) { if ( /\.js$/.test(moduleName) ) { const resource = await this.pool.findResourceWithInfo(moduleName); if (resource.info?.requiresTopLevelScope && !this.allowStringBundling) { this.logStringBundlingError(moduleName); continue; } let moduleContent = (await resource.buffer()).toString(); moduleContent = removeHashbang(moduleContent); let moduleSourceMap; if (this.options.sourceMap) { ({moduleContent, moduleSourceMap} = await this.getSourceMapForModule({ moduleName, moduleContent, resourcePath: resource.getPath() })); } const rewriteRes = await rewriteDefine({ moduleName, moduleContent, moduleSourceMap }); if (rewriteRes) { const {moduleContent, moduleSourceMap} = rewriteRes; outW.startSegment(moduleName); outW.ensureNewLine(); if (moduleSourceMap) { this.addSourceMap(moduleName, moduleSourceMap); } outW.write(moduleContent); outW.ensureNewLine(); const compressedSize = outW.endSegment(); log.verbose( ` ${moduleName} (${resource.info != null ? resource.info.size : -1},${compressedSize})`); } else { // keep unprocessed modules remaining.push(moduleName); } } else { // keep unprocessed modules remaining.push(moduleName); } } Array.prototype.splice.apply(sequence, [0, sequence.length].concat(remaining)); } /** * * @param {string} moduleName module name * @param {ModuleInfo} info * @param {@ui5/fs/Resource} resource * @returns {Promise<boolean>} */ async writePreloadModule(moduleName, info, resource) { const outW = this.outW; if ( /\.js$/.test(moduleName) && (info == null || !info.requiresTopLevelScope) ) { outW.writeln(`function(){`); // The module should be written to a new line in order for dev-tools to map breakpoints to it outW.ensureNewLine(); let moduleContent = (await resource.buffer()).toString(); moduleContent = removeHashbang(moduleContent); if (this.options.sourceMap) { let moduleSourceMap; ({moduleContent, moduleSourceMap} = await this.getSourceMapForModule({ moduleName, moduleContent, resourcePath: resource.getPath() })); this.addSourceMap(moduleName, moduleSourceMap); } outW.write(moduleContent); this.exportGlobalNames(info); outW.ensureNewLine(); outW.write(`}`); } else if ( /\.js$/.test(moduleName) /* implicitly: && info != null && info.requiresTopLevelScope */ ) { log.warn( `Module ${moduleName} requires top level scope and can only be embedded as a string (requires 'eval')`); let moduleContent = (await resource.buffer()).toString(); moduleContent = removeHashbang(moduleContent); if (this.options.sourceMap) { // We are actually not interested in the source map this module might contain, // but we should make sure to remove any "sourceMappingURL" from the module content before // writing it to the bundle. Otherwise browser dev-tools might create unnecessary // (and likely incorrect) requests for any referenced .map files ({moduleContent} = await this.getSourceMapForModule({ moduleName, moduleContent, resourcePath: resource.getPath() })); } outW.write( makeStringLiteral(moduleContent) ); } else if ( /\.html$/.test(moduleName) ) { const fileContent = (await resource.buffer()).toString(); outW.write( makeStringLiteral( fileContent ) ); } else if ( /\.json$/.test(moduleName) ) { let fileContent = (await resource.buffer()).toString(); if ( this.optimize ) { try { fileContent = JSON.stringify( JSON.parse( fileContent) ); } catch (e) { log.verbose(`Failed to parse JSON file ${moduleName}. Ignoring error, skipping compression.`); log.verbose(e); } } outW.write(makeStringLiteral(fileContent)); } else if ( /\.xml$/.test(moduleName) ) { let fileContent = (await resource.buffer()).toString(); 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, false); } } outW.write( makeStringLiteral( fileContent ) ); } else if ( /\.properties$/.test(moduleName) ) { // Since the Builder is also used when building non-project resources (e.g. dependencies) // *.properties files should be escaped if encoding option is specified const fileContent = await escapePropertiesFile(resource); outW.write( makeStringLiteral( fileContent ) ); } else { log.error("Don't know how to embed module " + moduleName); // TODO throw? } return true; } /** * Create exports for globals * * @param {ModuleInfo} info */ exportGlobalNames(info) { if ( !info || !info.exposedGlobals || !info.exposedGlobals.length ) { return; } this.outW.ensureNewLine(); info.exposedGlobals.forEach( (globalName) => { // Note: globalName can be assumed to be a valid identifier as it is used as variable name anyhow this.writeWithSourceMap(`this.${globalName}=${globalName};\n`); }); } logStringBundlingError(moduleName) { log.error( "Module " + moduleName + " requires top level scope and can only be embedded as a string " + "(requires 'eval'), which is not supported with specVersion 4.0 and higher. " + "For more information, see the UI5 CLI documentation " + "https://ui5.github.io/cli/stable/pages/Builder/#javascript-files-requiring-top-level-scope"); } async checkForStringBundling(moduleName) { if (!this.allowStringBundling && /\.js$/.test(moduleName)) { const resource = await this.pool.findResourceWithInfo(moduleName); if (resource.info?.requiresTopLevelScope) { this.logStringBundlingError(moduleName); return null; } } return moduleName; } async writeBundleInfos(sections) { this.outW.ensureNewLine(); let bundleInfoStr = ""; if ( sections.length > 0 ) { bundleInfoStr = "sap.ui.loader.config({bundlesUI5:{\n"; let initial = true; for (let idx = 0; idx < sections.length; idx++) { const section = sections[idx]; // Remove modules requiring string bundling let modules = await Promise.all(section.modules.map(this.checkForStringBundling.bind(this))); modules = modules.filter(($) => $) || []; if (!initial) { bundleInfoStr += ",\n"; } else { initial = false; } if (!section.name) { throw new Error(`A 'bundleInfo' section is missing the mandatory 'name' property.` ); } if (!path.extname(section.name)) { log.warn(`BundleInfo section name '${section.name}' is missing a file extension. ` + `The info might not work as expected. ` + `The name must match the bundle filename (incl. extension such as '.js')`); } bundleInfoStr += `"${section.name}":[${modules.map(makeStringLiteral).join(",")}]`; } bundleInfoStr += "\n}});\n"; this.writeWithSourceMap(bundleInfoStr); } } writeRequires(section) { if (section.modules.length === 0) { return; } this.outW.ensureNewLine(); 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 // are not duplicated across files. This might happen due to the filters provided. // So, certain modules that are included in depCache could be dependencies of another // module in the next file. This will also duplicate its dependency definition if we do not filter. #depCacheSet = new Set(); async writeDepCache(section) { let hasDepCache = false; const sequence = section.modules.slice().sort(); if (sequence.length > 0) { for (const module of sequence) { if (this.#depCacheSet.has(module)) { continue; } this.#depCacheSet.add(module); let resource = null; try { resource = await this.pool.findResourceWithInfo(module); } catch { log.error(` couldn't find ${module}`); } if (resource != null) { const deps = resource.info.dependencies.filter( (dep) => !resource.info.isConditionalDependency(dep) && !resource.info.isImplicitDependency(dep) ); if (deps.length > 0) { if (!hasDepCache) { hasDepCache = true; this.outW.ensureNewLine(); this.outW.writeln(`sap.ui.loader.config({depCacheUI5:{`); } this.outW.writeln( `"${module}": [${deps.map((dep) => `"${dep}"`).join(",")}],` ); } else { log.verbose(` skipped ${module}, no dependencies`); } } } if (hasDepCache) { this.outW.writeln(`}});`); } } } async getSourceMapForModule({moduleName, moduleContent, resourcePath}) { let moduleSourceMap = null; let newModuleContent = moduleContent; const sourceMapUrlMatch = moduleContent.match(sourceMappingUrlPattern); if (sourceMapUrlMatch) { const sourceMapUrl = sourceMapUrlMatch[1]; log.silly(`Found source map reference in content of module ${moduleName}: ${sourceMapUrl}`); // Strip sourceMappingURL from module code to be bundled // It has no effect and might be cause for confusion newModuleContent = moduleContent.replace(sourceMappingUrlPattern, ""); if (sourceMapUrl) { if (sourceMapUrl.startsWith("data:")) { // Data-URI indicates an inline source map const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,"; if (sourceMapUrl.startsWith(expectedTypeAndEncoding)) { const base64Content = sourceMapUrl.slice(expectedTypeAndEncoding.length); moduleSourceMap = Buffer.from(base64Content, "base64").toString(); } else { log.warn( `Source map reference in module ${moduleName} is a data URI but has an unexpected` + `encoding: ${sourceMapUrl}. Expected it to start with ` + `"data:application/json;charset=utf-8;base64,"`); } } else if (httpPattern.test(sourceMapUrl)) { log.warn(`Source map reference in module ${moduleName} is an absolute URL. ` + `Currently, only relative URLs are supported.`); } else if (path.posix.isAbsolute(sourceMapUrl)) { log.warn(`Source map reference in module ${moduleName} is an absolute path. ` + `Currently, only relative paths are supported.`); } else { const sourceMapPath = path.posix.join(path.posix.dirname(moduleName), sourceMapUrl); try { const sourceMapResource = await this.pool.findResource(sourceMapPath); moduleSourceMap = (await sourceMapResource.buffer()).toString(); } catch (e) { // No input source map log.warn(`Unable to read source map for module ${moduleName}: ${e.message}`); } } } } else { const sourceMapFileCandidate = resourcePath.slice("/resources/".length) + ".map"; log.silly(`Could not find a sourceMappingURL reference in content of module ${moduleName}. ` + `Attempting to find a source map resource based on the module's path: ${sourceMapFileCandidate}`); try { const sourceMapResource = await this.pool.findResource(sourceMapFileCandidate); moduleSourceMap = (await sourceMapResource.buffer()).toString(); } catch (e) { // No input source map log.silly(`Could not find a source map for module ${moduleName}: ${e.message}`); } } if (moduleSourceMap) { moduleSourceMap = JSON.parse(moduleSourceMap); // Check for index map, which is currently not supported if (Array.isArray(moduleSourceMap.sections)) { log.warn( `Module ${moduleName} references an index source map which is currently not supported. ` + `A transient source map will be created instead...` ); moduleSourceMap = createTransientSourceMap({ moduleName: path.posix.basename(resourcePath), moduleContent }); } } else { log.verbose(`No source map available for module ${moduleName}. Creating transient source map...`); moduleSourceMap = createTransientSourceMap({ moduleName: path.posix.basename(resourcePath), moduleContent }); } return { moduleSourceMap, moduleContent: newModuleContent }; } } const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"]; /* * @param {object} parameters * @param {string} parameters.moduleName * @param {string} parameters.moduleContent * @param {object} [parameters.moduleSourceMap] * @returns {Promise<object|null>} Object containing <code>moduleContent</code> and * <code>moduleSourceMap</code> (if one was provided) or <code>null</code> if no rewrite was applicable */ async function rewriteDefine({moduleName, moduleContent, moduleSourceMap}) { let ast; try { ast = parseJS(moduleContent, {range: true}); } catch (e) { log.error(`Error while parsing ${moduleName}: ${e.message}`); log.verbose(e.stack); return {}; } if ( ast.type === Syntax.Program && ast.body.length === 1 && ast.body[0].type === Syntax.ExpressionStatement && isMethodCall(ast.body[0].expression, CALL_SAP_UI_DEFINE) ) { const changes = []; const defineCall = ast.body[0].expression; // Inject module name if missing if ( defineCall.arguments.length == 0 || ![Syntax.Literal, Syntax.TemplateLiteral].includes(defineCall.arguments[0].type)) { let value = `"${toRequireJSName(moduleName)}"`; let index; if (defineCall.arguments.length == 0) { // asterisk marks the index: sap.ui.define(*) index = defineCall.range[1] - 1; } else { // asterisk marks the index: sap.ui.define(*argument1) index = defineCall.arguments[0].range[0]; value += ", "; } changes.push({ index, value }); } // 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" ) { changes.push({ // asterisk marks the index: sap.ui.*define() index: defineCall.callee.property.range[0], value: "pre" }); } return transform(changes, moduleContent, moduleSourceMap); } return null; } /* * @param {object[]} changes Changes that should be applied to the code * @param {string} moduleContent Code to transform * @param {object} [moduleSourceMap] Optional source map that should be aligned with the content change * @returns {Promise<object>} Object containing <code>moduleContent</code> and * <code>moduleSourceMap</code> (if one was provided) */ async function transform(changes, moduleContent, moduleSourceMap) { const mappingChanges = []; const array = Array.from(moduleContent); // No sorting needed as changes are added in correct (reverse) order changes.forEach((change) => { if (moduleSourceMap) { // Compute line and column for given index to re-align source map with inserted characters const precedingCode = array.slice(0, change.index); const line = precedingCode.reduce((lineCount, char) => { if (char === "\n") { lineCount++; } return lineCount; }, 0); const lineStartIndex = precedingCode.lastIndexOf("\n") + 1; const column = change.index - lineStartIndex; // Source map re-alignment needs to be done from front to back mappingChanges.unshift({ line, column, columnDiff: change.value.length }); } // Apply modification array.splice( change.index, 0, change.value ); }); const transformedCode = array.join(""); if (moduleSourceMap) { const mappings = decodeMappings(moduleSourceMap.mappings); mappingChanges.forEach((mappingChange) => { const lineMapping = mappings[mappingChange.line]; if (!lineMapping) { // No mapping available that could be transformed return; } // Mapping structure: // [generatedCodeColumn, sourceIndex, sourceCodeLine, sourceCodeColumn, nameIndex] lineMapping.forEach((mapping) => { if (mapping[0] > mappingChange.column) { // All column mappings for the generated code after any change // need to be moved by the amount of inserted characters mapping[0] = mapping[0] + mappingChange.columnDiff; } }); }); moduleSourceMap.mappings = encodeMappings(mappings); // No need for file information in source map since the bundled code does not exist in any file anyways delete moduleSourceMap.file; } return { moduleContent: transformedCode, moduleSourceMap }; } function createTransientSourceMap({moduleName, moduleContent, includeContent = false}) { const sourceMap = { version: 3, names: [], sources: [moduleName], // TODO: check whether moduleContent.match() with \n is better w.r.t performance/memory usage mappings: encodeMappings(moduleContent.split("\n").map((line, i) => { return [[0, 0, i, 0]]; })) }; if (includeContent) { sourceMap.sourcesContent = [moduleContent]; } return sourceMap; } export default BundleBuilder; export const __localFunctions__ = (process.env.NODE_ENV === "test") ? {rewriteDefine, createTransientSourceMap} : undefined;