}
*/
- async writePreloadModule(module, info, resource) {
+ async writePreloadModule(moduleName, info, resource) {
const outW = this.outW;
- if ( /\.js$/.test(module) && (info == null || !info.requiresTopLevelScope) ) {
- const compressedContent = await this.compressJS( await resource.buffer(), resource );
- outW.write(`function(){`);
- outW.write( compressedContent );
+ 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();
+ 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(module) /* implicitly: && info != null && info.requiresTopLevelScope */ ) {
+ } else if ( /\.js$/.test(moduleName) /* 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.compressJS( await resource.buffer(), resource );
- outW.write( makeStringLiteral( compressedContent ) );
- } else if ( /\.html$/.test(module) ) {
- const fileContent = await resource.buffer();
+ " and can only be embedded as a string (requires 'eval')", moduleName);
+ outW.write( makeStringLiteral( (await resource.buffer()).toString() ) );
+ } else if ( /\.html$/.test(moduleName) ) {
+ const fileContent = (await resource.buffer()).toString();
outW.write( makeStringLiteral( fileContent ) );
- } else if ( /\.json$/.test(module) ) {
- let fileContent = await resource.buffer();
+ } 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 %s. Ignoring error, skipping compression.", module);
+ log.verbose("Failed to parse JSON file %s. Ignoring error, skipping compression.", moduleName);
log.verbose(e);
}
}
outW.write(makeStringLiteral(fileContent));
- } else if ( /\.xml$/.test(module) ) {
- let fileContent = await resource.buffer();
+ } 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 should be preserved (should only happen rarely)
- if (!xmlHtmlPrePattern.test(fileContent.toString())) {
- fileContent = pd.xmlmin(fileContent.toString(), false);
+ if (!xmlHtmlPrePattern.test(fileContent)) {
+ fileContent = pd.xmlmin(fileContent, false);
}
}
outW.write( makeStringLiteral( fileContent ) );
- } else if ( /\.properties$/.test(module) ) {
+ } 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 " + module); // TODO throw?
+ log.error("don't know how to embed module " + moduleName); // TODO throw?
}
return true;
@@ -470,18 +535,19 @@ class BundleBuilder {
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.outW.writeln(`this.${globalName}=${globalName};`);
+ this.writeWithSourceMap(`this.${globalName}=${globalName};\n`);
});
}
writeBundleInfos(sections) {
this.outW.ensureNewLine();
+ let bundleInfoStr = "";
if ( sections.length > 0 ) {
- this.targetBundleFormat.beforeBundleInfo(this.outW);
+ bundleInfoStr = this.targetBundleFormat.beforeBundleInfo();
sections.forEach((section, idx) => {
if ( idx > 0 ) {
- this.outW.writeln(",");
+ bundleInfoStr += ",\n";
}
if (!section.name) {
@@ -492,31 +558,116 @@ class BundleBuilder {
`The info might not work as expected. ` +
`The name must match the bundle filename (incl. extension such as '.js')`);
}
- this.outW.write(`"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`);
+ bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
});
- this.outW.writeln();
- this.targetBundleFormat.afterBundleInfo(this.outW);
+ bundleInfoStr += "\n";
+ bundleInfoStr += this.targetBundleFormat.afterBundleInfo();
+
+ this.writeWithSourceMap(bundleInfoStr);
}
}
writeRequires(section) {
this.outW.ensureNewLine();
section.modules.forEach( (module) => {
- this.targetBundleFormat.requireSync(this.outW, module);
+ this.writeWithSourceMap(this.targetBundleFormat.requireSync(module));
});
}
+
+ async getSourceMapForModule({moduleName, moduleContent, resourcePath}) {
+ let moduleSourceMap = null;
+ let newModuleContent = moduleContent;
+
+ const sourceMapUrlMatch = moduleContent.match(sourceMappingUrlPattern);
+ if (sourceMapUrlMatch) {
+ const sourceMapUrl = sourceMapUrlMatch[1];
+ log.verbose(`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.verbose(`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);
+ if (sourceMapResource) {
+ moduleSourceMap = (await sourceMapResource.buffer()).toString();
+ }
+ } catch (e) {
+ // No input source map
+ log.verbose(`Could not find a source map for module ${moduleName}: ${e.message}`);
+ }
+ }
+
+
+ if (moduleSourceMap) {
+ moduleSourceMap = JSON.parse(moduleSourceMap);
+ } 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"];
-function rewriteDefine(targetBundleFormat, code, moduleName) {
+/*
+ * @param {object} parameters
+ * @param {string} parameters.moduleName
+ * @param {string} parameters.moduleContent
+ * @param {object} [parameters.moduleSourceMap]
+ * @returns {Promise