From 67ecb27e44a2d84e6b2203f31049220dcbcd41f0 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 9 Oct 2023 15:35:47 +0200 Subject: [PATCH] [FEATURE] Minifier: Support input source maps (#780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a resource references a source map (for example from an earlier transpilation): 1. Use that source map for the debug variant of the resource 2. Pass it to terser so it can update it based on the transformation, preserving the mapping to the initial source Before this change, the minifier processor always created a new source map based on the minification of the resource. This change is active by default. However, if a resource is changed before the `minify` task is executed (for example by tasks like `replaceCopyright`), we assume that the referenced source map is potentially corrupt and therefore ignore it. JIRA: CPOUI5FOUNDATION-467 --------- Co-authored-by: Matthias Osswald Co-authored-by: Günter Klatt <57760635+KlattG@users.noreply.github.com> --- lib/lbt/bundle/Builder.js | 13 +- lib/processors/minifier.js | 143 +++- lib/tasks/minify.js | 23 +- package-lock.json | 36 + package.json | 2 + .../standalone/resources/sap-ui-custom.js.map | 2 +- .../resources/sap-ui-core-nojQuery.js.map | 2 +- .../preload/resources/sap-ui-core.js.map | 2 +- .../sap/ui/core/library-preload.js.map | 2 +- .../sourcemaps/test.application/package.json | 6 + .../sourcemaps/test.application/ui5.yaml | 8 + ...avaScriptSourceWithCopyrightPlaceholder.js | 11 + .../webapp/TypeScriptSource.js | 2 + .../webapp/TypeScriptSource.js.map | 1 + .../webapp/TypeScriptSource.ts | 5 + .../test.application/webapp/manifest.json | 6 + test/lib/builder/sourceMaps.js | 156 +++++ test/lib/processors/minifier.js | 660 ++++++++++++++++++ test/lib/tasks/minify.integration.js | 263 +++++++ test/lib/tasks/minify.js | 398 +++++------ 20 files changed, 1494 insertions(+), 247 deletions(-) create mode 100644 test/fixtures/sourcemaps/test.application/package.json create mode 100644 test/fixtures/sourcemaps/test.application/ui5.yaml create mode 100644 test/fixtures/sourcemaps/test.application/webapp/JavaScriptSourceWithCopyrightPlaceholder.js create mode 100644 test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js create mode 100644 test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js.map create mode 100644 test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.ts create mode 100644 test/fixtures/sourcemaps/test.application/webapp/manifest.json create mode 100644 test/lib/builder/sourceMaps.js create mode 100644 test/lib/tasks/minify.integration.js diff --git a/lib/lbt/bundle/Builder.js b/lib/lbt/bundle/Builder.js index bfbacd880..ce204f411 100644 --- a/lib/lbt/bundle/Builder.js +++ b/lib/lbt/bundle/Builder.js @@ -18,7 +18,7 @@ import BundleWriter from "./BundleWriter.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("lbt:bundle:Builder"); -const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(.+)\s*$/; +const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/; const httpPattern = /^https?:\/\//i; const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/; @@ -332,11 +332,18 @@ class BundleBuilder { 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 + map.mappings = "AAAA," + map.mappings; } map.sourceRoot = path.posix.relative( @@ -592,9 +599,7 @@ class BundleBuilder { `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(); - } + moduleSourceMap = (await sourceMapResource.buffer()).toString(); } catch (e) { // No input source map log.silly(`Could not find a source map for module ${moduleName}: ${e.message}`); diff --git a/lib/processors/minifier.js b/lib/processors/minifier.js index ea2543a14..325463b38 100644 --- a/lib/processors/minifier.js +++ b/lib/processors/minifier.js @@ -1,5 +1,6 @@ import {fileURLToPath} from "node:url"; import posixPath from "node:path/posix"; +import {promisify} from "node:util"; import os from "node:os"; import workerpool from "workerpool"; import Resource from "@ui5/fs/Resource"; @@ -13,6 +14,9 @@ const MAX_WORKERS = 4; const osCpus = os.cpus().length || 1; const maxWorkers = Math.max(Math.min(osCpus - 1, MAX_WORKERS), MIN_WORKERS); +const sourceMappingUrlPattern = /\/\/# sourceMappingURL=(\S+)\s*$/; +const httpPattern = /^https?:\/\//i; + // Shared workerpool across all executions until the taskUtil cleanup is triggered let pool; @@ -38,6 +42,63 @@ async function minifyInWorker(options, taskUtil) { return getPool(taskUtil).exec("execMinification", [options]); } +async function extractAndRemoveSourceMappingUrl(resource) { + const resourceContent = await resource.getString(); + const resourcePath = resource.getPath(); + const sourceMappingUrlMatch = resourceContent.match(sourceMappingUrlPattern); + if (sourceMappingUrlMatch) { + const sourceMappingUrl = sourceMappingUrlMatch[1]; + if (log.isLevelEnabled("silly")) { + log.silly(`Found source map reference in content of resource ${resourcePath}: ${sourceMappingUrl}`); + } + + // Strip sourceMappingURL from the resource to be minified + // It is not required anymore and will be replaced for in the minified resource + // and its debug variant anyways + resource.setString(resourceContent.replace(sourceMappingUrlPattern, "")); + return sourceMappingUrl; + } + return null; +} + +async function getSourceMapFromUrl({sourceMappingUrl, resourcePath, readFile}) { + // ======================================================================= + // This code is almost identical to code located in lbt/bundle/Builder.js + // Please try to update both places when making changes + // ======================================================================= + if (sourceMappingUrl.startsWith("data:")) { + // Data-URI indicates an inline source map + const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,"; + if (sourceMappingUrl.startsWith(expectedTypeAndEncoding)) { + const base64Content = sourceMappingUrl.slice(expectedTypeAndEncoding.length); + // Create a resource with a path suggesting it's the source map for the resource + // (which it is but inlined) + return Buffer.from(base64Content, "base64").toString(); + } else { + log.warn( + `Source map reference in resource ${resourcePath} is a data URI but has an unexpected` + + `encoding: ${sourceMappingUrl}. Expected it to start with ` + + `"data:application/json;charset=utf-8;base64,"`); + } + } else if (httpPattern.test(sourceMappingUrl)) { + log.warn(`Source map reference in resource ${resourcePath} is an absolute URL. ` + + `Currently, only relative URLs are supported.`); + } else if (posixPath.isAbsolute(sourceMappingUrl)) { + log.warn(`Source map reference in resource ${resourcePath} is an absolute path. ` + + `Currently, only relative paths are supported.`); + } else { + const sourceMapPath = posixPath.join(posixPath.dirname(resourcePath), sourceMappingUrl); + + try { + const sourceMapContent = await readFile(sourceMapPath); + return sourceMapContent.toString(); + } catch (e) { + // No input source map + log.warn(`Unable to read source map for resource ${resourcePath}: ${e.message}`); + } + } +} + /** * @public * @module @ui5/builder/processors/minifier @@ -62,9 +123,16 @@ async function minifyInWorker(options, taskUtil) { * * @param {object} parameters Parameters * @param {@ui5/fs/Resource[]} parameters.resources List of resources to be processed + * @param {fs|module:@ui5/fs/fsInterface} parameters.fs Node fs or custom + * [fs interface]{@link module:@ui5/fs/fsInterface}. Required when setting "readSourceMappingUrl" to true * @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance. * Required when using the useWorkers option * @param {object} [parameters.options] Options + * @param {boolean} [parameters.options.readSourceMappingUrl=false] + * Whether to make use of any existing source maps referenced in the resources to be minified. Use this option to + * preserve references to the original source files, such as TypeScript files, in the generated source map.
+ * If a resource has been modified by a previous task, any existing source map will be ignored regardless of this + * setting. This is to ensure that no inconsistent source maps are used. Check the verbose log for details. * @param {boolean} [parameters.options.addSourceMappingUrl=true] * Whether to add a sourceMappingURL reference to the end of the minified resource * @param {boolean} [parameters.options.useWorkers=false] @@ -72,8 +140,14 @@ async function minifyInWorker(options, taskUtil) { * @returns {Promise} * Promise resolving with object of resource, dbgResource and sourceMap */ -export default async function({resources, taskUtil, options: {addSourceMappingUrl = true, useWorkers = false} = {}}) { +export default async function({ + resources, fs, taskUtil, options: {readSourceMappingUrl = false, addSourceMappingUrl = true, useWorkers = false + } = {}}) { let minify; + if (readSourceMappingUrl && !fs) { + throw new Error(`Option 'readSourceMappingUrl' requires parameter 'fs' to be provided`); + } + if (useWorkers) { if (!taskUtil) { // TaskUtil is required for worker support @@ -86,12 +160,11 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr } return Promise.all(resources.map(async (resource) => { - const dbgPath = resource.getPath().replace(debugFileRegex, "-dbg$1"); - const dbgResource = await resource.clone(); - dbgResource.setPath(dbgPath); + const resourcePath = resource.getPath(); + const dbgPath = resourcePath.replace(debugFileRegex, "-dbg$1"); + const dbgFilename = posixPath.basename(dbgPath); const filename = posixPath.basename(resource.getPath()); - const code = await resource.getString(); const sourceMapOptions = { filename @@ -99,7 +172,60 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr if (addSourceMappingUrl) { sourceMapOptions.url = filename + ".map"; } - const dbgFilename = posixPath.basename(dbgPath); + + // Remember contentModified flag before making changes to the resource via setString + const resourceContentModified = resource.getSourceMetadata()?.contentModified; + + // In any case: Extract *and remove* source map reference from resource before cloning it + const sourceMappingUrl = await extractAndRemoveSourceMappingUrl(resource); + + const code = await resource.getString(); + // Create debug variant based off the original resource before minification + const dbgResource = await resource.clone(); + dbgResource.setPath(dbgPath); + + let dbgSourceMapResource; + if (sourceMappingUrl) { + if (resourceContentModified) { + log.verbose( + `Source map found in resource will be ignored because the resource has been ` + + `modified in a previous task: ${resourcePath}`); + } else if (readSourceMappingUrl) { + // Try to find a source map reference in the to-be-minified resource + // If we find one, provide it to terser as an input source map and keep using it for the + // debug variant of the resource + const sourceMapContent = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath, + readFile: promisify(fs.readFile) + }); + + if (sourceMapContent) { + // Provide source map to terser as "input source map" + sourceMapOptions.content = sourceMapContent; + + // Also use the source map for the debug variant of the resource + // First update the file reference within the source map + const sourceMapJson = JSON.parse(sourceMapContent); + sourceMapJson.file = dbgFilename; + + // Then create a new resource + dbgSourceMapResource = new Resource({ + string: JSON.stringify(sourceMapJson), + path: dbgPath + ".map" + }); + dbgResource.setString(code + `//# sourceMappingURL=${dbgFilename}.map\n`); + } + } else { + // If the original resource content was unmodified and the input source map was not parsed, + // re-add the original source map reference to the debug variant + if (!sourceMappingUrl.startsWith("data:") && !sourceMappingUrl.endsWith(filename + ".map")) { + // Do not re-add inline source maps as well as references to the source map of + // the minified resource + dbgResource.setString(code + `//# sourceMappingURL=${sourceMappingUrl}\n`); + } + } + } const result = await minify({ filename, @@ -112,6 +238,9 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr path: resource.getPath() + ".map", string: result.map }); - return {resource, dbgResource, sourceMapResource}; + return {resource, dbgResource, sourceMapResource, dbgSourceMapResource}; })); } + +export const __localFunctions__ = (process.env.NODE_ENV === "test") ? + {getSourceMapFromUrl} : undefined; diff --git a/lib/tasks/minify.js b/lib/tasks/minify.js index 603f21288..7e32b13b4 100644 --- a/lib/tasks/minify.js +++ b/lib/tasks/minify.js @@ -1,4 +1,5 @@ import minifier from "../processors/minifier.js"; +import fsInterface from "@ui5/fs/fsInterface"; /** * @public @@ -19,20 +20,29 @@ import minifier from "../processors/minifier.js"; * @param {string} parameters.options.pattern Pattern to locate the files to be processed * @param {boolean} [parameters.options.omitSourceMapResources=false] Whether source map resources shall * be tagged as "OmitFromBuildResult" and no sourceMappingURL shall be added to the minified resource + * @param {boolean} [parameters.options.useInputSourceMaps=true] Whether to make use of any existing source + * maps referenced in the resources to be minified. Use this option to preserve reference to the original + * source files, such as TypeScript files, in the generated source map. * @returns {Promise} Promise resolving with undefined once data has been written */ -export default async function({workspace, taskUtil, options: {pattern, omitSourceMapResources = false}}) { +export default async function({ + workspace, taskUtil, options: {pattern, omitSourceMapResources = false, useInputSourceMaps = true + }}) { const resources = await workspace.byGlob(pattern); const processedResources = await minifier({ resources, + fs: fsInterface(workspace), taskUtil, options: { addSourceMappingUrl: !omitSourceMapResources, + readSourceMappingUrl: !!useInputSourceMaps, useWorkers: !!taskUtil, } }); - return Promise.all(processedResources.map(async ({resource, dbgResource, sourceMapResource}) => { + return Promise.all(processedResources.map(async ({ + resource, dbgResource, sourceMapResource, dbgSourceMapResource + }) => { if (taskUtil) { taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.HasDebugVariant); taskUtil.setTag(dbgResource, taskUtil.STANDARD_TAGS.IsDebugVariant); @@ -40,11 +50,18 @@ export default async function({workspace, taskUtil, options: {pattern, omitSourc if (omitSourceMapResources) { taskUtil.setTag(sourceMapResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); } + if (dbgSourceMapResource) { + taskUtil.setTag(dbgSourceMapResource, taskUtil.STANDARD_TAGS.IsDebugVariant); + if (omitSourceMapResources) { + taskUtil.setTag(dbgSourceMapResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult); + } + } } return Promise.all([ workspace.write(resource), workspace.write(dbgResource), - workspace.write(sourceMapResource) + workspace.write(sourceMapResource), + dbgSourceMapResource && workspace.write(dbgSourceMapResource) ]); })); } diff --git a/package-lock.json b/package-lock.json index d6179ecdc..fe92fe9b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.2.0", + "@jridgewell/trace-mapping": "^0.3.19", "@ui5/project": "^3.7.1", "ava": "^5.3.1", "chai": "^4.3.10", @@ -41,6 +42,7 @@ "eslint-plugin-ava": "^14.0.0", "eslint-plugin-jsdoc": "^46.8.2", "esmock": "^2.5.1", + "line-column": "^1.0.2", "nyc": "^15.1.0", "open-cli": "^7.2.0", "recursive-readdir": "^2.2.3", @@ -5157,6 +5159,24 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -5611,6 +5631,22 @@ "node": ">= 0.8.0" } }, + "node_modules/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", + "dev": true, + "dependencies": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, + "node_modules/line-column/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", diff --git a/package.json b/package.json index 94ccc7dbf..4e0f5eebc 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.2.0", + "@jridgewell/trace-mapping": "^0.3.19", "@ui5/project": "^3.7.1", "ava": "^5.3.1", "chai": "^4.3.10", @@ -150,6 +151,7 @@ "eslint-plugin-ava": "^14.0.0", "eslint-plugin-jsdoc": "^46.8.2", "esmock": "^2.5.1", + "line-column": "^1.0.2", "nyc": "^15.1.0", "open-cli": "^7.2.0", "recursive-readdir": "^2.2.3", diff --git a/test/expected/build/application.b/standalone/resources/sap-ui-custom.js.map b/test/expected/build/application.b/standalone/resources/sap-ui-custom.js.map index 67ccc4728..874425e28 100644 --- a/test/expected/build/application.b/standalone/resources/sap-ui-custom.js.map +++ b/test/expected/build/application.b/standalone/resources/sap-ui-custom.js.map @@ -1 +1 @@ -{"version":3,"file":"sap-ui-custom.js","sections":[{"offset":{"line":2,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":3,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":14,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":17,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":18,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}}]} \ No newline at end of file +{"version":3,"file":"sap-ui-custom.js","sections":[{"offset":{"line":2,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":3,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":14,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":17,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":18,"column":0},"map":{"version":3,"sources":["sap-ui-custom.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/test/expected/build/sap.ui.core/preload/resources/sap-ui-core-nojQuery.js.map b/test/expected/build/sap.ui.core/preload/resources/sap-ui-core-nojQuery.js.map index d0f3c75b7..22f1f2eeb 100644 --- a/test/expected/build/sap.ui.core/preload/resources/sap-ui-core-nojQuery.js.map +++ b/test/expected/build/sap.ui.core/preload/resources/sap-ui-core-nojQuery.js.map @@ -1 +1 @@ -{"version":3,"file":"sap-ui-core-nojQuery.js","sections":[{"offset":{"line":4,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":5,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":10,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":11,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-3"],"mappings":"AAAA;AACA;AACA","sourcesContent":["} catch(oError) {\nif (oError.name != \"Restart\") { throw oError; }\n}"],"sourceRoot":""}}]} \ No newline at end of file +{"version":3,"file":"sap-ui-core-nojQuery.js","sections":[{"offset":{"line":4,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":5,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":10,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":11,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"sources":["sap-ui-core-nojQuery.js?bundle-code-3"],"mappings":"AAAA;AACA;AACA","sourcesContent":["} catch(oError) {\nif (oError.name != \"Restart\") { throw oError; }\n}"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/test/expected/build/sap.ui.core/preload/resources/sap-ui-core.js.map b/test/expected/build/sap.ui.core/preload/resources/sap-ui-core.js.map index 2403f4fe7..3c34e4892 100644 --- a/test/expected/build/sap.ui.core/preload/resources/sap-ui-core.js.map +++ b/test/expected/build/sap.ui.core/preload/resources/sap-ui-core.js.map @@ -1 +1 @@ -{"version":3,"file":"sap-ui-core.js","sections":[{"offset":{"line":4,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":5,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":10,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":11,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-3"],"mappings":"AAAA;AACA;AACA","sourcesContent":["} catch(oError) {\nif (oError.name != \"Restart\") { throw oError; }\n}"],"sourceRoot":""}}]} \ No newline at end of file +{"version":3,"file":"sap-ui-core.js","sections":[{"offset":{"line":4,"column":0},"map":{"version":3,"file":"ui5loader-autoconfig.js","names":["thisIsTheUi5LoaderAutoconfig","console","log"],"sources":["ui5loader-autoconfig-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAA+B,KACnCC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":""}},{"offset":{"line":5,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"Core.js","names":["core","console","log"],"sources":["Core-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAAO,KACXC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"sap/ui/core"}},{"offset":{"line":10,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.requireSync(\"sap/ui/core/Core\");\n"],"sourceRoot":""}},{"offset":{"line":11,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-2"],"mappings":"AAAA;AACA","sourcesContent":["// as this module contains the Core, we ensure that the Core has been booted\nsap.ui.getCore().boot && sap.ui.getCore().boot();"],"sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"sources":["sap-ui-core.js?bundle-code-3"],"mappings":"AAAA;AACA;AACA","sourcesContent":["} catch(oError) {\nif (oError.name != \"Restart\") { throw oError; }\n}"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/test/expected/build/sap.ui.core/preload/resources/sap/ui/core/library-preload.js.map b/test/expected/build/sap.ui.core/preload/resources/sap/ui/core/library-preload.js.map index 6bff4624b..ab1c6971f 100644 --- a/test/expected/build/sap.ui.core/preload/resources/sap/ui/core/library-preload.js.map +++ b/test/expected/build/sap.ui.core/preload/resources/sap/ui/core/library-preload.js.map @@ -1 +1 @@ -{"version":3,"file":"library-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"sources":["library-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":3,"column":0},"map":{"version":3,"file":"one.js","names":["One"],"sources":["one-dbg.js"],"mappings":"AAAA,SAASA,MACR,OAAO,CACR","sourceRoot":""}},{"offset":{"line":4,"column":0},"map":{"version":3,"sources":["library-preload.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["this.One=One;\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"some.js","names":["console","log"],"sources":["some-dbg.js"],"mappings":"AAAA;;;AAGAA,QAAQC,IAAI","sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"file":"ui5loader.js","names":["thisIsTheUi5Loader","console","log"],"sources":["ui5loader-dbg.js"],"mappings":"CAAA,WACC,IAAIA,EAAqB,KACzBC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"../../.."}}]} \ No newline at end of file +{"version":3,"file":"library-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"sources":["library-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}},{"offset":{"line":3,"column":0},"map":{"version":3,"file":"one.js","names":["One"],"sources":["one-dbg.js"],"mappings":"AAAA,SAASA,MACR,OAAO,CACR","sourceRoot":""}},{"offset":{"line":4,"column":0},"map":{"version":3,"sources":["library-preload.js?bundle-code-1"],"mappings":"AAAA;AACA","sourcesContent":["this.One=One;\n"],"sourceRoot":""}},{"offset":{"line":7,"column":0},"map":{"version":3,"file":"some.js","names":["console","log"],"sources":["some-dbg.js"],"mappings":"AAAA;;;AAGAA,QAAQC,IAAI","sourceRoot":""}},{"offset":{"line":13,"column":0},"map":{"version":3,"file":"ui5loader.js","names":["thisIsTheUi5Loader","console","log"],"sources":["ui5loader-dbg.js"],"mappings":"AAAA,CAAA,WACC,IAAIA,EAAqB,KACzBC,QAAQC,IAAIF,EACZ,EAHD","sourceRoot":"../../.."}}]} \ No newline at end of file diff --git a/test/fixtures/sourcemaps/test.application/package.json b/test/fixtures/sourcemaps/test.application/package.json new file mode 100644 index 000000000..39b05f03a --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/package.json @@ -0,0 +1,6 @@ +{ + "name": "test..application", + "private": true, + "version": "1.0.0", + "description": "" +} diff --git a/test/fixtures/sourcemaps/test.application/ui5.yaml b/test/fixtures/sourcemaps/test.application/ui5.yaml new file mode 100644 index 000000000..1124b73a8 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/ui5.yaml @@ -0,0 +1,8 @@ +--- +specVersion: "3.1" +type: application +metadata: + name: test.application + copyright: | + Test Application + Copyright ${currentYear} diff --git a/test/fixtures/sourcemaps/test.application/webapp/JavaScriptSourceWithCopyrightPlaceholder.js b/test/fixtures/sourcemaps/test.application/webapp/JavaScriptSourceWithCopyrightPlaceholder.js new file mode 100644 index 000000000..aada6cb37 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/webapp/JavaScriptSourceWithCopyrightPlaceholder.js @@ -0,0 +1,11 @@ +/* + * ${copyright} + */ + +sap.ui.define((function() { + return { + functionWithinJavaScriptSourceWithCopyrightPlaceholder() { + functionCallWithinJavaScriptSourceWithCopyrightPlaceholder(); + } + } +})); diff --git a/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js new file mode 100644 index 000000000..669fb02b5 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js @@ -0,0 +1,2 @@ +"use strict";sap.ui.define([],function(){"use strict";var i={functionWithinTypeScriptSource(){functionCallWithinTypeScriptSource()}};return i}); +//# sourceMappingURL=TypeScriptSource.js.map \ No newline at end of file diff --git a/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js.map b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js.map new file mode 100644 index 000000000..b146bc061 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.js.map @@ -0,0 +1 @@ +{"version":3,"file":"TypeScriptSource.js","names":["functionWithinTypeScriptSource","functionCallWithinTypeScriptSource","__exports"],"sources":["TypeScriptSource.ts"],"sourcesContent":["export default {\n\tfunctionWithinTypeScriptSource() {\n\t\tfunctionCallWithinTypeScriptSource();\n\t}\n}\n"],"mappings":"4DAAe,CACdA,iCACCC,oCACD,GACA,OAAAC,CAAA"} \ No newline at end of file diff --git a/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.ts b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.ts new file mode 100644 index 000000000..589bfa498 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/webapp/TypeScriptSource.ts @@ -0,0 +1,5 @@ +export default { + functionWithinTypeScriptSource() { + functionCallWithinTypeScriptSource(); + } +} diff --git a/test/fixtures/sourcemaps/test.application/webapp/manifest.json b/test/fixtures/sourcemaps/test.application/webapp/manifest.json new file mode 100644 index 000000000..639d65e26 --- /dev/null +++ b/test/fixtures/sourcemaps/test.application/webapp/manifest.json @@ -0,0 +1,6 @@ +{ + "sap.app": { + "type": "application", + "id": "test.application" + } +} diff --git a/test/lib/builder/sourceMaps.js b/test/lib/builder/sourceMaps.js new file mode 100644 index 000000000..8fed6c8d3 --- /dev/null +++ b/test/lib/builder/sourceMaps.js @@ -0,0 +1,156 @@ +import test from "ava"; +import {readFile} from "node:fs/promises"; +import {fileURLToPath} from "node:url"; +import { + AnyMap, + originalPositionFor, +} from "@jridgewell/trace-mapping"; +import lineColumn from "line-column"; +import {graphFromPackageDependencies} from "@ui5/project/graph"; +import * as taskRepository from "../../../lib/tasks/taskRepository.js"; + +const applicationURL = new URL("../../fixtures/sourcemaps/test.application/", import.meta.url); +const applicationDestRootURL = new URL("../../tmp/build/sourcemaps/test.application/", import.meta.url); + +test.beforeEach((t) => { + const readDestFile = async (filePath) => { + return readFile(new URL(filePath, t.context.destURL), {encoding: "utf8"}); + }; + + t.context.assertSourceMapping = async function(t, { + generatedFilePath, + sourceFilePath, + code, + tracedName = undefined + }) { + const generatedFile = await readDestFile(generatedFilePath); + const sourceFile = await readDestFile(sourceFilePath); + const sourceMap = JSON.parse(await readDestFile(generatedFilePath + ".map")); + + if (sourceMap.sections) { + // @jridgewell/trace-mapping fails when a SectionedSourceMap doesn't have the "names" property. + // The property is not required according to the current schema, so it is a bug in trace-mapping. + // See https://github.com/SchemaStore/schemastore/blob/7d0dc50ea4532e1f18febd777919c477bf6e05f2/src/schemas/json/sourcemap-v3.json + sourceMap.sections.forEach((section) => { + if (!section.map.names) { + section.map.names = []; + } + }); + } + + const tracer = new AnyMap(sourceMap); + + const generatedCodeIndex = generatedFile.indexOf(code); + t.not(generatedCodeIndex, -1, `Code '${code}' must be present in generated code file '${generatedFilePath}'`); + + const codeLineColumn = lineColumn(generatedFile).fromIndex(generatedCodeIndex); + + const tracedCode = originalPositionFor(tracer, { + line: codeLineColumn.line, + column: codeLineColumn.col - 1 + }); + + t.is(tracedCode.source, sourceFilePath, + `Original position of code should be found in source file '${sourceFilePath}'`); + + if (tracedName) { + t.is(tracedCode.name, tracedName); + } + + const sourceCodeIndex = lineColumn(sourceFile).toIndex(tracedCode.line, tracedCode.column + 1); + t.is( + sourceFile.substring(sourceCodeIndex, sourceCodeIndex + code.length), code, + "Code should be at right place in source file" + ); + }; +}); + +test.serial("Verify source maps (test.application)", async (t) => { + const destURL = t.context.destURL = new URL("./dest-standard-build/", applicationDestRootURL); + + const graph = await graphFromPackageDependencies({ + cwd: fileURLToPath(applicationURL) + }); + graph.setTaskRepository(taskRepository); + + await graph.build({ + destPath: fileURLToPath(destURL) + }); + + // Default mapping created via minify task + await t.context.assertSourceMapping(t, { + generatedFilePath: "JavaScriptSourceWithCopyrightPlaceholder.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "sap.ui.define(", + tracedName: "sap" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "JavaScriptSourceWithCopyrightPlaceholder.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "functionWithinJavaScriptSourceWithCopyrightPlaceholder" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "JavaScriptSourceWithCopyrightPlaceholder.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "functionCallWithinJavaScriptSourceWithCopyrightPlaceholder()", + tracedName: "functionCallWithinJavaScriptSourceWithCopyrightPlaceholder" + }); + + // Mapping from debug variant to TypeScript source + await t.context.assertSourceMapping(t, { + generatedFilePath: "TypeScriptSource-dbg.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionWithinTypeScriptSource" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "TypeScriptSource-dbg.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionCallWithinTypeScriptSource()", + tracedName: "functionCallWithinTypeScriptSource" + }); + + // Mapping from minified Javascript to TypeScript source + await t.context.assertSourceMapping(t, { + generatedFilePath: "TypeScriptSource.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionWithinTypeScriptSource" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "TypeScriptSource.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionCallWithinTypeScriptSource()", + tracedName: "functionCallWithinTypeScriptSource" + }); + + // Mapping from Component-preload.js to JavaScript Source + await t.context.assertSourceMapping(t, { + generatedFilePath: "Component-preload.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "sap.ui.define(", + tracedName: "sap" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "Component-preload.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "functionWithinJavaScriptSourceWithCopyrightPlaceholder" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "Component-preload.js", + sourceFilePath: "JavaScriptSourceWithCopyrightPlaceholder-dbg.js", + code: "functionCallWithinJavaScriptSourceWithCopyrightPlaceholder()", + tracedName: "functionCallWithinJavaScriptSourceWithCopyrightPlaceholder" + }); + + // Mapping from Component-preload.js to TypeScript Source + await t.context.assertSourceMapping(t, { + generatedFilePath: "Component-preload.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionWithinTypeScriptSource" + }); + await t.context.assertSourceMapping(t, { + generatedFilePath: "Component-preload.js", + sourceFilePath: "TypeScriptSource.ts", + code: "functionCallWithinTypeScriptSource()", + tracedName: "functionCallWithinTypeScriptSource" + }); +}); diff --git a/test/lib/processors/minifier.js b/test/lib/processors/minifier.js index f2b9b000f..9cee61dcb 100644 --- a/test/lib/processors/minifier.js +++ b/test/lib/processors/minifier.js @@ -1,6 +1,7 @@ import test from "ava"; import sinon from "sinon"; import minifier from "../../../lib/processors/minifier.js"; +import {__localFunctions__} from "../../../lib/processors/minifier.js"; import {createResource} from "@ui5/fs/resourceFactory"; // Node.js itself tries to parse sourceMappingURLs in all JavaScript files. This is unwanted and might even lead to @@ -199,6 +200,556 @@ ${SOURCE_MAPPING_URL}=test3.designtime.js.map`; "Correct source map content for resource 3"); }); +test("Input source map: Incorrect parameters", async (t) => { + const content = `some content`; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + await t.throwsAsync(minifier({ + resources: [testResource], + options: { + readSourceMappingUrl: true, + } + }), { + message: `Option 'readSourceMappingUrl' requires parameter 'fs' to be provided` + }, "Threw with expected error message"); +}); + +test("Input source map: Provided inline", async (t) => { + const content = `/*! + * \${copyright} + */ +"use strict"; + +sap.ui.define(["sap/m/MessageBox", "./BaseController"], function (MessageBox, __BaseController) { + "use strict"; + + function _interopRequireDefault(obj) { + return obj && obj.__esModule && typeof obj.default !== "undefined" ? obj.default : obj; + } + const BaseController = _interopRequireDefault(__BaseController); + /** + * @namespace test.controller + */ + const Main = BaseController.extend("test.controller.Main", { + sayHello: function _sayHello() { + MessageBox.show("Hello World!"); + } + }); + return Main; +}); + +${SOURCE_MAPPING_URL}=data:application/json;charset=utf-8;base64,` + + `eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5jb250cm9sbGVyLmpzIiwibmFtZXMiOlsic2FwIiwidWkiLCJkZWZpbmUiLCJNZXNzYWdlQm94Ii` + + `wiX19CYXNlQ29udHJvbGxlciIsIl9pbnRlcm9wUmVxdWlyZURlZmF1bHQiLCJvYmoiLCJfX2VzTW9kdWxlIiwiZGVmYXVsdCIsIkJhc2VDb250` + + `cm9sbGVyIiwiTWFpbiIsImV4dGVuZCIsInNheUhlbGxvIiwiX3NheUhlbGxvIiwic2hvdyJdLCJzb3VyY2VzIjpbInRlc3QuY29udHJvbGxlci` + + `50cyJdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBRkE7O0FBQUFBLEdBQUEsQ0FBQUMsRUFBQSxDQUFBQyxNQUFBLHFEQUFBQyxVQUFB` + + `LEVBQUFDLGdCQUFBO0VBQUE7O0VBQUEsU0FBQUMsdUJBQUFDLEdBQUE7SUFBQSxPQUFBQSxHQUFBLElBQUFBLEdBQUEsQ0FBQUMsVUFBQSxXQU` + + `FBRCxHQUFBLENBQUFFLE9BQUEsbUJBQUFGLEdBQUEsQ0FBQUUsT0FBQSxHQUFBRixHQUFBO0VBQUE7RUFBQSxNQUlPRyxjQUFjLEdBQUFKLHNC` + + `QUFBLENBQUFELGdCQUFBO0VBRXJCO0FBQ0E7QUFDQTtFQUZBLE1BR3FCTSxJQUFJLEdBQVNELGNBQWMsQ0FBQUUsTUFBQTtJQUN4Q0MsUUFBUS` + + `xXQUFBQyxVQUFBLEVBQVM7TUFDdkJWLFVBQVUsQ0FBQ1csSUFBSSxDQUFDLGNBQWMsQ0FBQztJQUNoQztFQUFDO0VBQUEsT0FIbUJKLElBQUk7` + + `QUFBQSJ9`; + /* The above is a base64 encoded version of the following string + (identical to one in the next input source map test below): + `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox","__BaseController",` + + `"_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend","sayHello",` + + `"_sayHello","show"],"sources":["test.controller.ts"],"mappings":"AAAA;AACA;AACA;AAFA;;AAAAA,GAAA,CAAAC,` + + `EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,OAAAA,GAAA,IAAAA,GAAA,CAAAC,` + + `UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,cAAc,GAAAJ,sBAAA,CAAAD,` + + `gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,EAAS;MACvBV,UAAU,` + + `CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + */ + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true, + } + }); + + const expected = `/*! + * \${copyright} + */ +"use strict";sap.ui.define(["sap/m/MessageBox","./BaseController"],function(e,t){"use strict";function n(e){return ` + + `e&&e.__esModule&&typeof e.default!=="undefined"?e.default:e}const o=n(t);const s=o.extend(` + + `"test.controller.Main",{sayHello:function t(){e.show("Hello World!")}});return s}); +${SOURCE_MAPPING_URL}=test.controller.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + // Existing inline source map is moved into a separate file + const expectedDbgContent = content.replace(/data:application\/json;charset=utf-8;base64,.+/, "test-dbg.controller.js.map\n"); + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox",` + + `"__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend",` + + `"sayHello","_sayHello","show"],"sources":["test.controller.ts"],"mappings":";;;AAAA,aAAAA,IAAAC,GAAAC,OAAA,` + + `iDAAAC,EAAAC,GAAA,sBAAAC,EAAAC,GAAA,OAAAA,KAAAC,mBAAAD,EAAAE,UAAA,YAAAF,EAAAE,QAAAF,CAAA,OAIOG,EAAcJ,EAAAD,` + + `GAErB,MAGqBM,EAAaD,EAAcE,OAAA,wBACxCC,SAAQ,SAAAC,IACdV,EAAWW,KAAK,eACjB,IAAC,OAHmBJ,CAAI"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + const expectedDbgSourceMap = `{"version":3,"file":"test-dbg.controller.js","names":["sap","ui","define",` + + `"MessageBox","__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController",` + + `"Main","extend","sayHello","_sayHello","show"],"sources":["test.controller.ts"],"mappings":"AAAA;AACA;` + + `AACA;AAFA;;AAAAA,GAAA,CAAAC,EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,` + + `OAAAA,GAAA,IAAAA,GAAA,CAAAC,UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,` + + `cAAc,GAAAJ,sBAAA,CAAAD,gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,` + + `EAAS;MACvBV,UAAU,CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + t.deepEqual(await dbgSourceMapResource.getString(), expectedDbgSourceMap, + "Correct source map content for debug variant "); +}); + +test("Input source map: Provided in separate map file", async (t) => { + const content = `/*! + * \${copyright} + */ +"use strict"; + +sap.ui.define(["sap/m/MessageBox", "./BaseController"], function (MessageBox, __BaseController) { + "use strict"; + + function _interopRequireDefault(obj) { + return obj && obj.__esModule && typeof obj.default !== "undefined" ? obj.default : obj; + } + const BaseController = _interopRequireDefault(__BaseController); + /** + * @namespace test.controller + */ + const Main = BaseController.extend("test.controller.Main", { + sayHello: function _sayHello() { + MessageBox.show("Hello World!"); + } + }); + return Main; +}); + +${SOURCE_MAPPING_URL}=test.controller.js.map +`; + + const inputSourceMapContent = + `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox","__BaseController",` + + `"_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend","sayHello",` + + `"_sayHello","show"],"sources":["test.controller.ts"],"mappings":"AAAA;AACA;AACA;AAFA;;AAAAA,GAAA,CAAAC,` + + `EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,OAAAA,GAAA,IAAAA,GAAA,CAAAC,` + + `UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,cAAc,GAAAJ,sBAAA,CAAAD,` + + `gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,EAAS;MACvBV,UAAU,` + + `CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + switch (filePath) { + case "/resources/test.controller.js.map": + cb(null, inputSourceMapContent); + return; + } + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true, + } + }); + + const expected = `/*! + * \${copyright} + */ +"use strict";sap.ui.define(["sap/m/MessageBox","./BaseController"],function(e,t){"use strict";function n(e){return ` + + `e&&e.__esModule&&typeof e.default!=="undefined"?e.default:e}const o=n(t);const s=o.extend(` + + `"test.controller.Main",{sayHello:function t(){e.show("Hello World!")}});return s}); +${SOURCE_MAPPING_URL}=test.controller.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace("test.controller.js.map", "test-dbg.controller.js.map"); + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox",` + + `"__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend",` + + `"sayHello","_sayHello","show"],"sources":["test.controller.ts"],"mappings":";;;AAAA,aAAAA,IAAAC,GAAAC,OAAA,` + + `iDAAAC,EAAAC,GAAA,sBAAAC,EAAAC,GAAA,OAAAA,KAAAC,mBAAAD,EAAAE,UAAA,YAAAF,EAAAE,QAAAF,CAAA,OAIOG,EAAcJ,EAAAD,` + + `GAErB,MAGqBM,EAAaD,EAAcE,OAAA,wBACxCC,SAAQ,SAAAC,IACdV,EAAWW,KAAK,eACjB,IAAC,OAHmBJ,CAAI"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + const expectedDbgSourceMap = `{"version":3,"file":"test-dbg.controller.js","names":["sap","ui","define",` + + `"MessageBox","__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController",` + + `"Main","extend","sayHello","_sayHello","show"],"sources":["test.controller.ts"],"mappings":"AAAA;AACA;` + + `AACA;AAFA;;AAAAA,GAAA,CAAAC,EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,` + + `OAAAA,GAAA,IAAAA,GAAA,CAAAC,UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,` + + `cAAc,GAAAJ,sBAAA,CAAAD,gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,` + + `EAAS;MACvBV,UAAU,CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + t.deepEqual(await dbgSourceMapResource.getString(), expectedDbgSourceMap, + "Correct source map content for debug variant "); +}); + +test("Input source map: Provided inline with sources content", async (t) => { + const content = `/*! + * \${copyright} + */ +"use strict"; + +sap.ui.define(["sap/m/MessageBox", "./BaseController"], function (MessageBox, __BaseController) { + "use strict"; + + function _interopRequireDefault(obj) { + return obj && obj.__esModule && typeof obj.default !== "undefined" ? obj.default : obj; + } + const BaseController = _interopRequireDefault(__BaseController); + /** + * @namespace test.controller + */ + const Main = BaseController.extend("test.controller.Main", { + sayHello: function _sayHello() { + MessageBox.show("Hello World!"); + } + }); + return Main; +}); + +${SOURCE_MAPPING_URL}=data:application/json;charset=utf-8;base64,` + + `eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5jb250cm9sbGVyLmpzIiwibmFtZXMiOlsic2FwIiwidWkiLCJkZWZpbmUiLCJNZXNzYWdlQm94I` + + `iwiX19CYXNlQ29udHJvbGxlciIsIl9pbnRlcm9wUmVxdWlyZURlZmF1bHQiLCJvYmoiLCJfX2VzTW9kdWxlIiwiZGVmYXVsdCIsIkJhc2VDb2` + + `50cm9sbGVyIiwiTWFpbiIsImV4dGVuZCIsInNheUhlbGxvIiwiX3NheUhlbGxvIiwic2hvdyJdLCJzb3VyY2VzIjpbInRlc3QuY29udHJvbGx` + + `lci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKiFcbiAqICR7Y29weXJpZ2h0fVxuICovXG5pbXBvcnQgTWVzc2FnZUJveCBmcm9tIFwic2Fw` + + `L20vTWVzc2FnZUJveFwiO1xuaW1wb3J0IEJhc2VDb250cm9sbGVyIGZyb20gXCIuL0Jhc2VDb250cm9sbGVyXCI7XG5cbi8qKlxuICogQG5hb` + + `WVzcGFjZSBjb20ubWIudHMudGVzdGFwcC5jb250cm9sbGVyXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIE1haW4gZXh0ZW5kcyBCYXNlQ2` + + `9udHJvbGxlciB7XG5cdHB1YmxpYyBzYXlIZWxsbygpOiB2b2lkIHtcblx0TWVzc2FnZUJveC5zaG93KFwiSGVsbG8gV29ybGQhXCIpO1xuXHR` + + `9XG59XG4iXSwibWFwcGluZ3MiOiJBQUFBO0FBQ0E7QUFDQTtBQUZBOztBQUFBQSxHQUFBLENBQUFDLEVBQUEsQ0FBQUMsTUFBQSxxREFBQUMs` + + `VUFBQSxFQUFBQyxnQkFBQTtFQUFBOztFQUFBLFNBQUFDLHVCQUFBQyxHQUFBO0lBQUEsT0FBQUEsR0FBQSxJQUFBQSxHQUFBLENBQUFDLFVBQ` + + `UEsV0FBQUQsR0FBQSxDQUFBRSxPQUFBLG1CQUFBRixHQUFBLENBQUFFLE9BQUEsR0FBQUYsR0FBQTtFQUFBO0VBQUEsTUFJT0csY0FBYyxHQU` + + `FBSixzQkFBQSxDQUFBRCxnQkFBQTtFQUVyQjtBQUNBO0FBQ0E7RUFGQSxNQUdxQk0sSUFBSSxHQUFTRCxjQUFjLENBQUFFLE1BQUE7SUFDeEN` + + `DLFFBQVEsV0FBQUMsVUFBQSxFQUFTO01BQ3ZCVixVQUFVLENBQUNXLElBQUksQ0FBQyxjQUFjLENBQUM7SUFDaEM7RUFBQztFQUFBLE9BSG1C` + + `SixJQUFJO0FBQUEifQ==`; + + /* The above is a base64 encoded version of the following string + (identical to one in the next input source map test below): */ + // eslint-disable-next-line + // {"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox","__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend","sayHello","_sayHello","show"],"sources":["test.controller.ts"],"sourcesContent":["/*!\n * ${copyright}\n */\nimport MessageBox from \"sap/m/MessageBox\";\nimport BaseController from \"./BaseController\";\n\n/**\n * @namespace com.mb.ts.testapp.controller\n */\nexport default class Main extends BaseController {\n\tpublic sayHello(): void {\n\tMessageBox.show(\"Hello World!\");\n\t}\n}\n"],"mappings":"AAAA;AACA;AACA;AAFA;;AAAAA,GAAA,CAAAC,EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,OAAAA,GAAA,IAAAA,GAAA,CAAAC,UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,cAAc,GAAAJ,sBAAA,CAAAD,gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,EAAS;MACvBV,UAAU,CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"} + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true, + } + }); + + const expected = `/*! + * \${copyright} + */ +"use strict";sap.ui.define(["sap/m/MessageBox","./BaseController"],function(e,t){"use strict";function n(e){return ` + + `e&&e.__esModule&&typeof e.default!=="undefined"?e.default:e}const o=n(t);const s=o.extend(` + + `"test.controller.Main",{sayHello:function t(){e.show("Hello World!")}});return s}); +${SOURCE_MAPPING_URL}=test.controller.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + // Existing inline source map is moved into a separate file + // Both source maps still contain the "sourcesContent" attribute + const expectedDbgContent = content.replace(/data:application\/json;charset=utf-8;base64,.+/, "test-dbg.controller.js.map\n"); + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox",` + + `"__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend",` + + `"sayHello","_sayHello","show"],"sources":["test.controller.ts"],"sourcesContent":["/*!\\n * \${copyright}` + + `\\n */\\nimport MessageBox from \\"sap/m/MessageBox\\";\\nimport BaseController from \\"./BaseController\\";` + + `\\n\\n/**\\n * @namespace com.mb.ts.testapp.controller\\n */\\nexport default class Main extends ` + + `BaseController {\\n\\tpublic sayHello(): void {\\n\\tMessageBox.show(\\"Hello World!\\");\\n\\t}\\n}\\n"],` + + `"mappings":";;;AAAA,aAAAA,IAAAC,GAAAC,OAAA,` + + `iDAAAC,EAAAC,GAAA,sBAAAC,EAAAC,GAAA,OAAAA,KAAAC,mBAAAD,EAAAE,UAAA,YAAAF,EAAAE,QAAAF,CAAA,OAIOG,EAAcJ,EAAAD,` + + `GAErB,MAGqBM,EAAaD,EAAcE,OAAA,wBACxCC,SAAQ,SAAAC,IACdV,EAAWW,KAAK,eACjB,IAAC,OAHmBJ,CAAI"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + const expectedDbgSourceMap = `{"version":3,"file":"test-dbg.controller.js","names":["sap","ui","define",` + + `"MessageBox","__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController",` + + `"Main","extend","sayHello","_sayHello","show"],"sources":["test.controller.ts"],"sourcesContent":["/*!\\n` + + ` * \${copyright}\\n */\\nimport MessageBox from \\"sap/m/MessageBox\\";\\nimport BaseController from ` + + `\\"./BaseController\\";\\n\\n/**\\n * @namespace com.mb.ts.testapp.controller\\n */\\nexport default class ` + + `Main extends BaseController {\\n\\tpublic sayHello(): void {\\n\\tMessageBox.show(\\"Hello World!\\");` + + `\\n\\t}\\n}\\n"],` + + `"mappings":"AAAA;AACA;` + + `AACA;AAFA;;AAAAA,GAAA,CAAAC,EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,` + + `OAAAA,GAAA,IAAAA,GAAA,CAAAC,UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,` + + `cAAc,GAAAJ,sBAAA,CAAAD,gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,` + + `EAAS;MACvBV,UAAU,CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + t.deepEqual(await dbgSourceMapResource.getString(), expectedDbgSourceMap, + "Correct source map content for debug variant "); +}); + +test("Input source map: Reference is ignored in default configuration", async (t) => { + const content = `/*! + * \${copyright} + */ +"use strict"; + +sap.ui.define(["sap/m/MessageBox", "./BaseController"], function (MessageBox, __BaseController) { + "use strict"; + + function _interopRequireDefault(obj) { + return obj && obj.__esModule && typeof obj.default !== "undefined" ? obj.default : obj; + } + const BaseController = _interopRequireDefault(__BaseController); + /** + * @namespace test.controller + */ + const Main = BaseController.extend("test.controller.Main", { + sayHello: function _sayHello() { + MessageBox.show("Hello World!"); + } + }); + return Main; +}); + +${SOURCE_MAPPING_URL}=test.controller.js.map +`; + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs + }); + + const expected = `/*! + * \${copyright} + */ +"use strict";sap.ui.define(["sap/m/MessageBox","./BaseController"],function(e,t){"use strict";function n(e){return ` + + `e&&e.__esModule&&typeof e.default!=="undefined"?e.default:e}const o=n(t);const s=o.extend(` + + `"test.controller.Main",{sayHello:function t(){e.show("Hello World!")}});return s}); +${SOURCE_MAPPING_URL}=test.controller.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace(/\/\/#.*\s+$/, ""); // Remove sourceMappingURL reference + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox",` + + `"__BaseController","_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend",` + + `"sayHello","_sayHello","show"],"sources":["test-dbg.controller.js"],"mappings":";;;AAGA,aAEAA,IAAIC,GAAGC,OAAO,` + + `CAAC,mBAAoB,oBAAqB,SAAUC,EAAYC,GAC5E,aAEA,SAASC,EAAuBC,GAC9B,OAAOA,GAAOA,EAAIC,mBAAqBD,EAAIE,UAAY,YAAcF,EAAIE,` + + `QAAUF,CACrF,CACA,MAAMG,EAAiBJ,EAAuBD,GAI9C,MAAMM,EAAOD,EAAeE,OAAO,uBAAwB,CACzDC,SAAU,SAASC,IACjBV,EAAWW,KAAK,` + + `eAClB,IAEF,OAAOJ,CACT"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + t.is(dbgSourceMapResource, undefined, + "No source map resource has been created for the debug variant resource"); +}); + +test("Input source map: Inline source map is ignored in default configuration", async (t) => { + const content = `console.log("Hello"); +${SOURCE_MAPPING_URL}=data:application/json;charset=utf-8;base64,foo +`; + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/test.controller.js", + string: content + }); + const [{resource, dbgResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs + }); + + const expected = `console.log("Hello"); +${SOURCE_MAPPING_URL}=test.controller.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace(/\/\/#.*\s+$/, ""); // Remove sourceMappingURL reference + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + t.is(dbgSourceMapResource, undefined, + "No source map resource has been created for the debug variant resource"); +}); + +test("Input source map: Resource has been modified by previous tasks", async (t) => { + const content = `"use strict"; + +/*! + * (c) Copyright Test File + * Demo Content Only + */ +console.log("Hello"); +${SOURCE_MAPPING_URL}=Demo.view.js.map +`; + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/Demo.view.js", + string: content, + sourceMetadata: { + contentModified: true // Flag content as modified + } + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true + } + }); + + const expected = `"use strict"; +/*! + * (c) Copyright Test File + * Demo Content Only + */console.log("Hello"); +${SOURCE_MAPPING_URL}=Demo.view.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace(/\/\/#.*\s+$/, ""); // Remove sourceMappingURL reference + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"Demo.view.js","names":["console","log"],` + + `"sources":["Demo-dbg.view.js"],"mappings":"AAAA;;;;GAMAA,QAAQC,IAAI"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + t.is(dbgSourceMapResource, undefined, + "No source map resource has been created for the debug variant resource"); +}); + +test("Input source map: Non-standard name referenced", async (t) => { + const content = `"use strict"; + +/*! + * (c) Copyright Test File + * Demo Content Only + */ +console.log("Hello"); +${SOURCE_MAPPING_URL}=../different-name.map +`; + const inputSourceMapContent = + `{"version":3,"file":"Demo.view.js","names":["console","log"],"sources":["Demo.view.ts"],` + + `"mappings":";;AAAA;AACA;AACA;AACA;AACCA,OAAO,CAACC,GAAG,CAAC,OAAO,CAAC"}`; + + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + switch (filePath) { + case "/different-name.map": + cb(null, inputSourceMapContent); + return; + } + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/Demo.view.js", + string: content + }); + const [{resource, dbgResource, sourceMapResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true + } + }); + + const expected = `"use strict"; +/*! + * (c) Copyright Test File + * Demo Content Only + */console.log("Hello"); +${SOURCE_MAPPING_URL}=Demo.view.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace("../different-name.map", "Demo-dbg.view.js.map"); + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + const expectedSourceMap = `{"version":3,"file":"Demo.view.js","names":["console","log"],` + + `"sources":["Demo.view.ts"],"mappings":";;;;GAICA,QAAQC,IAAI"}`; + t.deepEqual(await sourceMapResource.getString(), expectedSourceMap, "Correct source map content"); + t.is(await dbgSourceMapResource.getString(), inputSourceMapContent.replace("Demo.view.js", "Demo-dbg.view.js"), + "Correct source map content for debug variant"); +}); + +test("Input source map: HTTP URL is ignored", async (t) => { + const content = `"use strict"; + +/*! + * (c) Copyright Test File + * Demo Content Only + */ +console.log("Hello"); +${SOURCE_MAPPING_URL}=https://ui5.sap.com/resources/my/test/module.js.map +`; + const fs = { + readFile: sinon.stub().callsFake((filePath, cb) => { + // We don't expect this test to read any files, so always throw an error here + const err = new Error("ENOENT: no such file or directory, open " + filePath); + err.code = "ENOENT"; + cb(err); + }) + }; + + const testResource = createResource({ + path: "/resources/Test.js", + string: content + }); + const [{resource, dbgResource, dbgSourceMapResource}] = await minifier({ + resources: [testResource], + fs, + options: { + readSourceMappingUrl: true + } + }); + + const expected = `"use strict"; +/*! + * (c) Copyright Test File + * Demo Content Only + */console.log("Hello"); +${SOURCE_MAPPING_URL}=Test.js.map`; + t.deepEqual(await resource.getString(), expected, "Correct minified content"); + const expectedDbgContent = content.replace(/\/\/#.*\s+$/, ""); // Remove sourceMappingURL reference + t.deepEqual(await dbgResource.getString(), expectedDbgContent, "Correct debug content"); + t.is(dbgSourceMapResource, undefined, + "No source map resource has been created for the debug variant resource"); +}); + test("Different copyright", async (t) => { const content = ` /* @@ -388,4 +939,113 @@ this code can't be parsed!`; t.regex(error.message, /line/, "Error should contain line"); }); +test("getSourceMapFromUrl: Base64", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub(); + const sourceMappingUrl = `data:application/json;charset=utf-8;base64,` + + `eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGVzdC5jb250cm9sbGVyLmpzIiwibmFtZXMiOlsic2FwIiwidWkiLCJkZWZpbmUiLCJNZXNzYWdlQm94Ii` + + `wiX19CYXNlQ29udHJvbGxlciIsIl9pbnRlcm9wUmVxdWlyZURlZmF1bHQiLCJvYmoiLCJfX2VzTW9kdWxlIiwiZGVmYXVsdCIsIkJhc2VDb250` + + `cm9sbGVyIiwiTWFpbiIsImV4dGVuZCIsInNheUhlbGxvIiwiX3NheUhlbGxvIiwic2hvdyJdLCJzb3VyY2VzIjpbInRlc3QuY29udHJvbGxlci` + + `50cyJdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBRkE7O0FBQUFBLEdBQUEsQ0FBQUMsRUFBQSxDQUFBQyxNQUFBLHFEQUFBQyxVQUFB` + + `LEVBQUFDLGdCQUFBO0VBQUE7O0VBQUEsU0FBQUMsdUJBQUFDLEdBQUE7SUFBQSxPQUFBQSxHQUFBLElBQUFBLEdBQUEsQ0FBQUMsVUFBQSxXQU` + + `FBRCxHQUFBLENBQUFFLE9BQUEsbUJBQUFGLEdBQUEsQ0FBQUUsT0FBQSxHQUFBRixHQUFBO0VBQUE7RUFBQSxNQUlPRyxjQUFjLEdBQUFKLHNC` + + `QUFBLENBQUFELGdCQUFBO0VBRXJCO0FBQ0E7QUFDQTtFQUZBLE1BR3FCTSxJQUFJLEdBQVNELGNBQWMsQ0FBQUUsTUFBQTtJQUN4Q0MsUUFBUS` + + `xXQUFBQyxVQUFBLEVBQVM7TUFDdkJWLFVBQVUsQ0FBQ1csSUFBSSxDQUFDLGNBQWMsQ0FBQztJQUNoQztFQUFDO0VBQUEsT0FIbUJKLElBQUk7` + + `QUFBQSJ9`; + // The above is a base64 encoded version of the following string + // (identical to one in the inline-input source map test somewhere above): + const decodedSourceMap = + `{"version":3,"file":"test.controller.js","names":["sap","ui","define","MessageBox","__BaseController",` + + `"_interopRequireDefault","obj","__esModule","default","BaseController","Main","extend","sayHello",` + + `"_sayHello","show"],"sources":["test.controller.ts"],"mappings":"AAAA;AACA;AACA;AAFA;;AAAAA,GAAA,CAAAC,` + + `EAAA,CAAAC,MAAA,qDAAAC,UAAA,EAAAC,gBAAA;EAAA;;EAAA,SAAAC,uBAAAC,GAAA;IAAA,OAAAA,GAAA,IAAAA,GAAA,CAAAC,` + + `UAAA,WAAAD,GAAA,CAAAE,OAAA,mBAAAF,GAAA,CAAAE,OAAA,GAAAF,GAAA;EAAA;EAAA,MAIOG,cAAc,GAAAJ,sBAAA,CAAAD,` + + `gBAAA;EAErB;AACA;AACA;EAFA,MAGqBM,IAAI,GAASD,cAAc,CAAAE,MAAA;IACxCC,QAAQ,WAAAC,UAAA,EAAS;MACvBV,UAAU,` + + `CAACW,IAAI,CAAC,cAAc,CAAC;IAChC;EAAC;EAAA,OAHmBJ,IAAI;AAAA"}`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, decodedSourceMap, "Expected source map content"); + t.is(readFileStub.callCount, 0, "No files have been read"); +}); +test("getSourceMapFromUrl: Unexpected data: format", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub(); + const sourceMappingUrl = `data:something/json;charset=utf-8;base64,foo`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, undefined, "No source map content returned"); + t.is(readFileStub.callCount, 0, "No files have been read"); +}); + +test("getSourceMapFromUrl: File reference", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub().resolves(new Buffer("Source Map Content")); + const sourceMappingUrl = `./other/file.map`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, "Source Map Content", "Expected source map content"); + t.is(readFileStub.callCount, 1, "One file has been read"); + t.is(readFileStub.firstCall.firstArg, "/some/other/file.map", "Correct file has been read"); +}); + +test("getSourceMapFromUrl: File reference not found", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub().rejects(new Error("Not found")); + const sourceMappingUrl = `./other/file.map`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, undefined, "No source map content returned"); // Error is suppressed + t.is(readFileStub.callCount, 1, "One file has been read"); + t.is(readFileStub.firstCall.firstArg, "/some/other/file.map", "Correct file has been read"); +}); + +test("getSourceMapFromUrl: HTTPS URL reference", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub().resolves(new Buffer("Source Map Content")); + const sourceMappingUrl = `https://ui5.sap.com/resources/my/test/module.js.map`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, undefined, "No source map content returned"); + t.is(readFileStub.callCount, 0, "No files has been read"); +}); + +test("getSourceMapFromUrl: Absolute path reference", async (t) => { + const {getSourceMapFromUrl} = __localFunctions__; + const readFileStub = sinon.stub().resolves(new Buffer("Source Map Content")); + const sourceMappingUrl = `/some/file.map`; + + const res = await getSourceMapFromUrl({ + sourceMappingUrl, + resourcePath: "/some/module.js", + readFile: readFileStub + }); + + t.is(res, undefined, "No source map content returned"); + t.is(readFileStub.callCount, 0, "No files has been read"); +}); diff --git a/test/lib/tasks/minify.integration.js b/test/lib/tasks/minify.integration.js new file mode 100644 index 000000000..3e90a8519 --- /dev/null +++ b/test/lib/tasks/minify.integration.js @@ -0,0 +1,263 @@ +import test from "ava"; +import sinon from "sinon"; +import minify from "../../../lib/tasks/minify.js"; +import * as resourceFactory from "@ui5/fs/resourceFactory"; +import DuplexCollection from "@ui5/fs/DuplexCollection"; + +// Node.js itself tries to parse sourceMappingURLs in all JavaScript files. This is unwanted and might even lead to +// obscure errors when dynamically generating Data-URI soruceMappingURL values. +// Therefore use this constant to never write the actual string. +const SOURCE_MAPPING_URL = "//" + "# sourceMappingURL"; + +function createWorkspace() { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const workspace = new DuplexCollection({reader: reader, writer: writer}); + return {reader, writer, workspace}; +} + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test("integration: minify omitSourceMapResources=true", async (t) => { + const taskUtil = { + setTag: sinon.stub(), + STANDARD_TAGS: { + HasDebugVariant: "1️⃣", + IsDebugVariant: "2️⃣", + OmitFromBuildResult: "3️⃣" + }, + registerCleanupTask: sinon.stub() + }; + const {reader, writer, workspace} = createWorkspace(); + const content = ` +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + await reader.write(testResource); + + await minify({ + workspace, + taskUtil, + options: { + pattern: "/test.js", + omitSourceMapResources: true + } + }); + + const expected = `function test(t){var o=t;console.log(o)}test();`; + const res = await writer.byPath("/test.js"); + if (!res) { + t.fail("Could not find /test.js in target locator"); + } + t.deepEqual(await res.getString(), expected, "Correct file content"); + + const resDbg = await writer.byPath("/test-dbg.js"); + if (!resDbg) { + t.fail("Could not find /test-dbg.js in target locator"); + } + t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); + + const expectedSourceMap = + `{"version":3,"file":"test.js",` + + `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + + `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; + + const resSourceMap = await writer.byPath("/test.js.map"); + if (!resSourceMap) { + t.fail("Could not find /test-dbg.js.map in target locator"); + } + t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); + + t.is(taskUtil.setTag.callCount, 4, "taskUtil.setTag was called 4 times"); + t.is(taskUtil.setTag.getCall(0).args[0].getPath(), res.getPath(), + "First taskUtil.setTag call with expected first argument"); + t.is(taskUtil.setTag.getCall(0).args[1], "1️⃣", "First taskUtil.setTag call with expected second argument"); + t.is(taskUtil.setTag.getCall(1).args[0].getPath(), resDbg.getPath(), + "Second taskUtil.setTag call with expected first arguments"); + t.is(taskUtil.setTag.getCall(1).args[1], "2️⃣", + "Second taskUtil.setTag call with expected second arguments"); + t.is(taskUtil.setTag.getCall(2).args[0].getPath(), resSourceMap.getPath(), + "Third taskUtil.setTag call with expected first arguments"); + t.is(taskUtil.setTag.getCall(2).args[1], "1️⃣", + "Third taskUtil.setTag call with expected second arguments"); + t.is(taskUtil.setTag.getCall(3).args[0].getPath(), resSourceMap.getPath(), + "Fourth taskUtil.setTag call with expected first arguments"); + t.is(taskUtil.setTag.getCall(3).args[1], "3️⃣", + "Fourth taskUtil.setTag call with expected second arguments"); +}); + +test("integration: minify omitSourceMapResources=false", async (t) => { + const taskUtil = { + setTag: sinon.stub(), + STANDARD_TAGS: { + HasDebugVariant: "1️⃣", + IsDebugVariant: "2️⃣", + OmitFromBuildResult: "3️⃣" + } + }; + const {reader, writer, workspace} = createWorkspace(); + const content = ` +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + await reader.write(testResource); + + await minify({ + workspace, + taskUtil, + options: { + pattern: "/test.js" + } + }); + + const expected = `function test(t){var o=t;console.log(o)}test(); +${SOURCE_MAPPING_URL}=test.js.map`; + const res = await writer.byPath("/test.js"); + if (!res) { + t.fail("Could not find /test.js in target locator"); + } + t.deepEqual(await res.getString(), expected, "Correct file content"); + + const resDbg = await writer.byPath("/test-dbg.js"); + if (!resDbg) { + t.fail("Could not find /test-dbg.js in target locator"); + } + t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); + + const expectedSourceMap = + `{"version":3,"file":"test.js",` + + `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + + `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; + + const resSourceMap = await writer.byPath("/test.js.map"); + if (!resSourceMap) { + t.fail("Could not find /test-dbg.js.map in target locator"); + } + t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); + + t.is(taskUtil.setTag.callCount, 3, "taskUtil.setTag was called 3 times"); + t.is(taskUtil.setTag.getCall(0).args[0].getPath(), res.getPath(), + "First taskUtil.setTag call with expected first argument"); + t.is(taskUtil.setTag.getCall(0).args[1], "1️⃣", "First taskUtil.setTag call with expected second argument"); + t.is(taskUtil.setTag.getCall(1).args[0].getPath(), resDbg.getPath(), + "Second taskUtil.setTag call with expected first arguments"); + t.is(taskUtil.setTag.getCall(1).args[1], "2️⃣", + "Second taskUtil.setTag call with expected second arguments"); + t.is(taskUtil.setTag.getCall(2).args[0].getPath(), resSourceMap.getPath(), + "Third taskUtil.setTag call with expected first arguments"); + t.is(taskUtil.setTag.getCall(2).args[1], "1️⃣", + "Third taskUtil.setTag call with expected second arguments"); +}); + +test("integration: minify omitSourceMapResources=true (without taskUtil)", async (t) => { + const {reader, writer, workspace} = createWorkspace(); + const content = ` +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + await reader.write(testResource); + + await minify({ + workspace, + options: { + pattern: "/test.js", + omitSourceMapResources: true + } + }); + + const expected = `function test(t){var o=t;console.log(o)}test();`; + const res = await writer.byPath("/test.js"); + if (!res) { + t.fail("Could not find /test.js in target locator"); + } + t.deepEqual(await res.getString(), expected, "Correct file content"); + + const resDbg = await writer.byPath("/test-dbg.js"); + if (!resDbg) { + t.fail("Could not find /test-dbg.js in target locator"); + } + t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); + + const expectedSourceMap = + `{"version":3,"file":"test.js",` + + `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + + `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; + + const resSourceMap = await writer.byPath("/test.js.map"); + if (!resSourceMap) { + t.fail("Could not find /test-dbg.js.map in target locator"); + } + t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); +}); + +test("integration: minify omitSourceMapResources=false (without taskUtil)", async (t) => { + const {reader, writer, workspace} = createWorkspace(); + const content = ` +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + await reader.write(testResource); + + await minify({ + workspace, + options: { + pattern: "/test.js", + omitSourceMapResources: false + } + }); + + const expected = `function test(t){var o=t;console.log(o)}test(); +${SOURCE_MAPPING_URL}=test.js.map`; + const res = await writer.byPath("/test.js"); + if (!res) { + t.fail("Could not find /test.js in target locator"); + } + t.deepEqual(await res.getString(), expected, "Correct file content"); + + const resDbg = await writer.byPath("/test-dbg.js"); + if (!resDbg) { + t.fail("Could not find /test-dbg.js in target locator"); + } + t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); + + const expectedSourceMap = + `{"version":3,"file":"test.js",` + + `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + + `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; + + const resSourceMap = await writer.byPath("/test.js.map"); + if (!resSourceMap) { + t.fail("Could not find /test-dbg.js.map in target locator"); + } + t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); +}); diff --git a/test/lib/tasks/minify.js b/test/lib/tasks/minify.js index 3e90a8519..086bc1df4 100644 --- a/test/lib/tasks/minify.js +++ b/test/lib/tasks/minify.js @@ -1,263 +1,203 @@ import test from "ava"; -import sinon from "sinon"; -import minify from "../../../lib/tasks/minify.js"; -import * as resourceFactory from "@ui5/fs/resourceFactory"; -import DuplexCollection from "@ui5/fs/DuplexCollection"; - -// Node.js itself tries to parse sourceMappingURLs in all JavaScript files. This is unwanted and might even lead to -// obscure errors when dynamically generating Data-URI soruceMappingURL values. -// Therefore use this constant to never write the actual string. -const SOURCE_MAPPING_URL = "//" + "# sourceMappingURL"; - -function createWorkspace() { - const reader = resourceFactory.createAdapter({ - virBasePath: "/" - }); - const writer = resourceFactory.createAdapter({ - virBasePath: "/" - }); - const workspace = new DuplexCollection({reader: reader, writer: writer}); - return {reader, writer, workspace}; -} - -test.afterEach.always((t) => { - sinon.restore(); -}); - -test("integration: minify omitSourceMapResources=true", async (t) => { - const taskUtil = { +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + t.context.workspace = { + byGlob: sinon.stub().resolves(["resource A", "resource B"]), + write: sinon.stub().resolves() + }; + t.context.taskUtil = { setTag: sinon.stub(), STANDARD_TAGS: { - HasDebugVariant: "1️⃣", - IsDebugVariant: "2️⃣", - OmitFromBuildResult: "3️⃣" + HasDebugVariant: "has debug variant", + IsDebugVariant: "is debug variant", + OmitFromBuildResult: "omit from build result" }, registerCleanupTask: sinon.stub() }; - const {reader, writer, workspace} = createWorkspace(); - const content = ` -function test(paramA) { - var variableA = paramA; - console.log(variableA); -} -test();`; - const testResource = resourceFactory.createResource({ - path: "/test.js", - string: content - }); - await reader.write(testResource); - await minify({ - workspace, - taskUtil, - options: { - pattern: "/test.js", - omitSourceMapResources: true - } + t.context.fsInterfaceStub = sinon.stub().returns("fs interface"); + t.context.minifierStub = sinon.stub(); + t.context.minify = await esmock("../../../lib/tasks/minify.js", { + "@ui5/fs/fsInterface": t.context.fsInterfaceStub, + "../../../lib/processors/minifier.js": t.context.minifierStub }); - - const expected = `function test(t){var o=t;console.log(o)}test();`; - const res = await writer.byPath("/test.js"); - if (!res) { - t.fail("Could not find /test.js in target locator"); - } - t.deepEqual(await res.getString(), expected, "Correct file content"); - - const resDbg = await writer.byPath("/test-dbg.js"); - if (!resDbg) { - t.fail("Could not find /test-dbg.js in target locator"); - } - t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); - - const expectedSourceMap = - `{"version":3,"file":"test.js",` + - `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + - `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; - - const resSourceMap = await writer.byPath("/test.js.map"); - if (!resSourceMap) { - t.fail("Could not find /test-dbg.js.map in target locator"); - } - t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); - - t.is(taskUtil.setTag.callCount, 4, "taskUtil.setTag was called 4 times"); - t.is(taskUtil.setTag.getCall(0).args[0].getPath(), res.getPath(), - "First taskUtil.setTag call with expected first argument"); - t.is(taskUtil.setTag.getCall(0).args[1], "1️⃣", "First taskUtil.setTag call with expected second argument"); - t.is(taskUtil.setTag.getCall(1).args[0].getPath(), resDbg.getPath(), - "Second taskUtil.setTag call with expected first arguments"); - t.is(taskUtil.setTag.getCall(1).args[1], "2️⃣", - "Second taskUtil.setTag call with expected second arguments"); - t.is(taskUtil.setTag.getCall(2).args[0].getPath(), resSourceMap.getPath(), - "Third taskUtil.setTag call with expected first arguments"); - t.is(taskUtil.setTag.getCall(2).args[1], "1️⃣", - "Third taskUtil.setTag call with expected second arguments"); - t.is(taskUtil.setTag.getCall(3).args[0].getPath(), resSourceMap.getPath(), - "Fourth taskUtil.setTag call with expected first arguments"); - t.is(taskUtil.setTag.getCall(3).args[1], "3️⃣", - "Fourth taskUtil.setTag call with expected second arguments"); +}); +test.afterEach.always((t) => { + t.context.sinon.restore(); }); -test("integration: minify omitSourceMapResources=false", async (t) => { - const taskUtil = { - setTag: sinon.stub(), - STANDARD_TAGS: { - HasDebugVariant: "1️⃣", - IsDebugVariant: "2️⃣", - OmitFromBuildResult: "3️⃣" - } - }; - const {reader, writer, workspace} = createWorkspace(); - const content = ` -function test(paramA) { - var variableA = paramA; - console.log(variableA); -} -test();`; - const testResource = resourceFactory.createResource({ - path: "/test.js", - string: content - }); - await reader.write(testResource); - +test("minify: Default params", async (t) => { + const {minify, workspace, taskUtil, minifierStub} = t.context; + minifierStub.resolves([{ + resource: "resource A", + dbgResource: "dbgResource A", + sourceMapResource: "sourceMapResource A", + dbgSourceMapResource: "dbgSourceMapResource A" // optional + }, { + resource: "resource B", + dbgResource: "dbgResource B", + sourceMapResource: "sourceMapResource B", + }]); await minify({ workspace, taskUtil, options: { - pattern: "/test.js" + pattern: "**" } }); - const expected = `function test(t){var o=t;console.log(o)}test(); -${SOURCE_MAPPING_URL}=test.js.map`; - const res = await writer.byPath("/test.js"); - if (!res) { - t.fail("Could not find /test.js in target locator"); - } - t.deepEqual(await res.getString(), expected, "Correct file content"); - - const resDbg = await writer.byPath("/test-dbg.js"); - if (!resDbg) { - t.fail("Could not find /test-dbg.js in target locator"); - } - t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); - - const expectedSourceMap = - `{"version":3,"file":"test.js",` + - `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + - `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; - - const resSourceMap = await writer.byPath("/test.js.map"); - if (!resSourceMap) { - t.fail("Could not find /test-dbg.js.map in target locator"); - } - t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); - - t.is(taskUtil.setTag.callCount, 3, "taskUtil.setTag was called 3 times"); - t.is(taskUtil.setTag.getCall(0).args[0].getPath(), res.getPath(), - "First taskUtil.setTag call with expected first argument"); - t.is(taskUtil.setTag.getCall(0).args[1], "1️⃣", "First taskUtil.setTag call with expected second argument"); - t.is(taskUtil.setTag.getCall(1).args[0].getPath(), resDbg.getPath(), - "Second taskUtil.setTag call with expected first arguments"); - t.is(taskUtil.setTag.getCall(1).args[1], "2️⃣", - "Second taskUtil.setTag call with expected second arguments"); - t.is(taskUtil.setTag.getCall(2).args[0].getPath(), resSourceMap.getPath(), - "Third taskUtil.setTag call with expected first arguments"); - t.is(taskUtil.setTag.getCall(2).args[1], "1️⃣", - "Third taskUtil.setTag call with expected second arguments"); -}); - -test("integration: minify omitSourceMapResources=true (without taskUtil)", async (t) => { - const {reader, writer, workspace} = createWorkspace(); - const content = ` -function test(paramA) { - var variableA = paramA; - console.log(variableA); -} -test();`; - const testResource = resourceFactory.createResource({ - path: "/test.js", - string: content + t.is(minifierStub.callCount, 1, "minifier got called once"); + const minifierCallArgs = minifierStub.firstCall.firstArg; + t.deepEqual(minifierCallArgs.resources, ["resource A", "resource B"], "Correct resources provided to processor"); + t.is(minifierCallArgs.fs, "fs interface", "Correct fs interface provided to processor"); + t.is(minifierCallArgs.taskUtil, taskUtil, "Correct taskUtil provided to processor"); + t.deepEqual(minifierCallArgs.options, { + addSourceMappingUrl: true, + readSourceMappingUrl: true, + useWorkers: true + }, "minifier got called with expected options"); + + t.is(taskUtil.setTag.callCount, 7, "taskUtil#setTag got called 12 times"); + t.is(taskUtil.setTag.getCall(0).args[0], "resource A", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(0).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(1).args[0], "dbgResource A", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(1).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(2).args[0], "sourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(2).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(3).args[0], "dbgSourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(3).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + + t.is(taskUtil.setTag.getCall(4).args[0], "resource B", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(4).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(5).args[0], "dbgResource B", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(5).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(6).args[0], "sourceMapResource B", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(6).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + + t.is(workspace.write.callCount, 7, "workspace#write got called seven times"); + [ + "resource A", "dbgResource A", "sourceMapResource A", "dbgSourceMapResource A", + "resource B", "dbgResource B", "sourceMapResource B" + ].forEach((resName, idx) => { + t.is(workspace.write.getCall(idx).firstArg, resName, "workspace#write got called for expected resource"); }); - await reader.write(testResource); +}); +test("minify: omitSourceMapResources: true, useInputSourceMaps: false", async (t) => { + const {minify, workspace, taskUtil, minifierStub} = t.context; + minifierStub.resolves([{ + resource: "resource A", + dbgResource: "dbgResource A", + sourceMapResource: "sourceMapResource A", + dbgSourceMapResource: "dbgSourceMapResource A" // optional + }, { + resource: "resource B", + dbgResource: "dbgResource B", + sourceMapResource: "sourceMapResource B", + }]); await minify({ workspace, + taskUtil, options: { - pattern: "/test.js", - omitSourceMapResources: true + pattern: "**", + omitSourceMapResources: true, + useInputSourceMaps: false } }); - const expected = `function test(t){var o=t;console.log(o)}test();`; - const res = await writer.byPath("/test.js"); - if (!res) { - t.fail("Could not find /test.js in target locator"); - } - t.deepEqual(await res.getString(), expected, "Correct file content"); - - const resDbg = await writer.byPath("/test-dbg.js"); - if (!resDbg) { - t.fail("Could not find /test-dbg.js in target locator"); - } - t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); - - const expectedSourceMap = - `{"version":3,"file":"test.js",` + - `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + - `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; - - const resSourceMap = await writer.byPath("/test.js.map"); - if (!resSourceMap) { - t.fail("Could not find /test-dbg.js.map in target locator"); - } - t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); -}); - -test("integration: minify omitSourceMapResources=false (without taskUtil)", async (t) => { - const {reader, writer, workspace} = createWorkspace(); - const content = ` -function test(paramA) { - var variableA = paramA; - console.log(variableA); -} -test();`; - const testResource = resourceFactory.createResource({ - path: "/test.js", - string: content + t.is(minifierStub.callCount, 1, "minifier got called once"); + const minifierCallArgs = minifierStub.firstCall.firstArg; + t.deepEqual(minifierCallArgs.resources, ["resource A", "resource B"], "Correct resources provided to processor"); + t.is(minifierCallArgs.fs, "fs interface", "Correct fs interface provided to processor"); + t.is(minifierCallArgs.taskUtil, taskUtil, "Correct taskUtil provided to processor"); + t.deepEqual(minifierCallArgs.options, { + addSourceMappingUrl: false, + readSourceMappingUrl: false, + useWorkers: true + }, "minifier got called with expected options"); + + t.is(taskUtil.setTag.callCount, 10, "taskUtil#setTag got called 12 times"); + t.is(taskUtil.setTag.getCall(0).args[0], "resource A", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(0).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(1).args[0], "dbgResource A", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(1).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(2).args[0], "sourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(2).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(3).args[0], "sourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(3).args[1], "omit from build result", + "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(4).args[0], "dbgSourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(4).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(5).args[0], "dbgSourceMapResource A", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(5).args[1], "omit from build result", + "taskUtil#setTag got called with the correct tag"); + + t.is(taskUtil.setTag.getCall(6).args[0], "resource B", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(6).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(7).args[0], "dbgResource B", "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(7).args[1], "is debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(8).args[0], "sourceMapResource B", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(8).args[1], "has debug variant", "taskUtil#setTag got called with the correct tag"); + t.is(taskUtil.setTag.getCall(9).args[0], "sourceMapResource B", + "taskUtil#setTag got called with the correct resource"); + t.is(taskUtil.setTag.getCall(9).args[1], "omit from build result", + "taskUtil#setTag got called with the correct tag"); + + t.is(workspace.write.callCount, 7, "workspace#write got called seven times"); + [ + "resource A", "dbgResource A", "sourceMapResource A", "dbgSourceMapResource A", + "resource B", "dbgResource B", "sourceMapResource B" + ].forEach((resName, idx) => { + t.is(workspace.write.getCall(idx).firstArg, resName, "workspace#write got called for expected resource"); }); - await reader.write(testResource); +}); +test("minify: No taskUtil", async (t) => { + const {minify, workspace, minifierStub} = t.context; + minifierStub.resolves([{ + resource: "resource A", + dbgResource: "dbgResource A", + sourceMapResource: "sourceMapResource A", + dbgSourceMapResource: "dbgSourceMapResource A" // optional + }, { + resource: "resource B", + dbgResource: "dbgResource B", + sourceMapResource: "sourceMapResource B", + }]); await minify({ workspace, options: { - pattern: "/test.js", - omitSourceMapResources: false + pattern: "**" } }); - const expected = `function test(t){var o=t;console.log(o)}test(); -${SOURCE_MAPPING_URL}=test.js.map`; - const res = await writer.byPath("/test.js"); - if (!res) { - t.fail("Could not find /test.js in target locator"); - } - t.deepEqual(await res.getString(), expected, "Correct file content"); - - const resDbg = await writer.byPath("/test-dbg.js"); - if (!resDbg) { - t.fail("Could not find /test-dbg.js in target locator"); - } - t.deepEqual(await resDbg.getString(), content, "Correct debug-file content"); - - const expectedSourceMap = - `{"version":3,"file":"test.js",` + - `"names":["test","paramA","variableA","console","log"],"sources":["test-dbg.js"],` + - `"mappings":"AACA,SAASA,KAAKC,GACb,IAAIC,EAAYD,EAChBE,QAAQC,IAAIF,EACb,CACAF"}`; - - const resSourceMap = await writer.byPath("/test.js.map"); - if (!resSourceMap) { - t.fail("Could not find /test-dbg.js.map in target locator"); - } - t.deepEqual(await resSourceMap.getString(), expectedSourceMap, "Correct source map content"); + t.is(minifierStub.callCount, 1, "minifier got called once"); + const minifierCallArgs = minifierStub.firstCall.firstArg; + t.deepEqual(minifierCallArgs.resources, ["resource A", "resource B"], "Correct resources provided to processor"); + t.is(minifierCallArgs.fs, "fs interface", "Correct fs interface provided to processor"); + t.is(minifierCallArgs.taskUtil, undefined, "No taskUtil provided to processor"); + t.deepEqual(minifierCallArgs.options, { + addSourceMappingUrl: true, + readSourceMappingUrl: true, + useWorkers: false + }, "minifier got called with expected options"); + + t.is(workspace.write.callCount, 7, "workspace#write got called seven times"); + [ + "resource A", "dbgResource A", "sourceMapResource A", "dbgSourceMapResource A", + "resource B", "dbgResource B", "sourceMapResource B" + ].forEach((resName, idx) => { + t.is(workspace.write.getCall(idx).firstArg, resName, "workspace#write got called for expected resource"); + }); });