diff --git a/lib/tasks/generateCachebusterInfo.js b/lib/tasks/generateCachebusterInfo.js index a1fa96be7..fe4d7cddc 100644 --- a/lib/tasks/generateCachebusterInfo.js +++ b/lib/tasks/generateCachebusterInfo.js @@ -1,4 +1,31 @@ const resourceFactory = require("@ui5/fs").resourceFactory; +const crypto = require("crypto"); + +async function signByTime(resource) { + return resource.getStatInfo().mtime.getTime(); +} + +async function signByHash(resource) { + const hasher = crypto.createHash("sha1"); + const buffer = await resource.getBuffer(); + + hasher.update(buffer.toString("binary")); + return hasher.digest("hex"); +} + +function getSigner(type) { + type = type || "time"; + + switch (type) { + case "time": + return signByTime; + case "hash": + return signByHash; + + default: + throw new Error(`Invalid signature type: '${type}'. Valid ones are: 'time' or 'hash'`); + } +} /** * Task to generate the application cachebuster info file. @@ -10,6 +37,7 @@ const resourceFactory = require("@ui5/fs").resourceFactory; * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files * @param {Object} parameters.options Options * @param {string} parameters.options.namespace Namespace of the application + * @param {string} [parameters.options.signatureType='time'] Type of signature to be used ('time' or 'hash') * @returns {Promise} Promise resolving with undefined once data has been written */ module.exports = function({workspace, dependencies, options}) { @@ -17,10 +45,12 @@ module.exports = function({workspace, dependencies, options}) { .then(async (resources) => { const cachebusterInfo = {}; const regex = new RegExp(`^/resources/${options.namespace}/`); - resources.forEach((resource) => { + const signer = getSigner(options.signatureType); + + await Promise.all(resources.map(async (resource) => { const normalizedPath = resource.getPath().replace(regex, ""); - cachebusterInfo[normalizedPath] = resource.getStatInfo().mtime.getTime(); - }); + cachebusterInfo[normalizedPath] = await signer(resource); + })); const cachebusterInfoResource = resourceFactory.createResource({ path: `/resources/${options.namespace}/sap-ui-cachebuster-info.json`, string: JSON.stringify(cachebusterInfo, null, 2) diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index 644788f75..0f7a393da 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -171,7 +171,10 @@ class ApplicationBuilder extends AbstractBuilder { workspace: resourceCollections.workspace, dependencies: resourceCollections.dependencies, options: { - namespace: project.metadata.namespace + namespace: project.metadata.namespace, + signatureType: project.builder + && project.builder.cachebuster + && project.builder.cachebuster.signatureType, } }); }); diff --git a/test/lib/tasks/generateCachebusterInfo.js b/test/lib/tasks/generateCachebusterInfo.js index cdf94ea69..97031cd9f 100644 --- a/test/lib/tasks/generateCachebusterInfo.js +++ b/test/lib/tasks/generateCachebusterInfo.js @@ -57,6 +57,39 @@ test("integration: Build application.g with manifestBundler", (t) => { }); }); +test("integration: Build application.g with manifestBundler and cachebuster using hashes", (t) => { + const destPath = path.join("test", "tmp", "build", "application.g", "cachebuster_hash"); + const expectedPath = path.join("test", "expected", "build", "application.g", "cachebuster"); + const excludedTasks = ["generateVersionInfo"]; + const includedTasks = ["generateCachebusterInfo"]; + + return builder.build({ + tree: applicationGTreeWithCachebusterHash, + destPath, + excludedTasks, + includedTasks + }).then(() => { + return findFiles(expectedPath); + }).then((expectedFiles) => { + // Check for all directories and files + assert.directoryDeepEqual(destPath, expectedPath); + + // Check for all file contents + expectedFiles.forEach((expectedFile) => { + const relativeFile = path.relative(expectedPath, expectedFile); + const destFile = path.join(destPath, relativeFile); + if (expectedFile.endsWith("sap-ui-cachebuster-info.json")) { + const currentContent = JSON.parse(fs.readFileSync(destFile, "utf-8").replace(/(:\s+)("[^"]+")/g, ": \"\"")); + const expectedContent = JSON.parse(fs.readFileSync(expectedFile, "utf-8").replace(/(:\s+)(\d+)/g, ": \"\"")); + assert.deepEqual(currentContent, expectedContent); + } else { + assert.fileEqual(destFile, expectedFile); + } + }); + t.pass(); + }); +}); + const applicationGTree = { "id": "application.g", "version": "1.0.0", @@ -82,3 +115,33 @@ const applicationGTree = { } } }; + +const applicationGTreeWithCachebusterHash = { + "id": "application.g", + "version": "1.0.0", + "path": applicationGPath, + "dependencies": [], + "builder": { + "cachebuster": { + "signatureType": "hash" + } + }, + "_level": 0, + "specVersion": "0.1", + "type": "application", + "metadata": { + "name": "application.g", + "namespace": "application.g", + "copyright": "Some fancy copyright" + }, + "resources": { + "configuration": { + "paths": { + "webapp": "webapp" + } + }, + "pathMappings": { + "/": "webapp" + } + } +};