Skip to content

Commit

Permalink
[FEATURE] Minifier: Support input source maps
Browse files Browse the repository at this point in the history
If a resource references a source map (or if we find a source map based
on the resource name + ".map"):
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
  • Loading branch information
RandomByte committed Aug 25, 2023
1 parent 39b2b38 commit 9c31fda
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 9 deletions.
128 changes: 121 additions & 7 deletions lib/processors/minifier.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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*$/;
const httpPattern = /^https?:\/\//i;

// Shared workerpool across all executions until the taskUtil cleanup is triggered
let pool;

Expand All @@ -38,6 +42,75 @@ async function minifyInWorker(options, taskUtil) {
return getPool(taskUtil).exec("execMinification", [options]);
}

async function getSourceMap(resource, readFile) {
const resourceContent = await resource.getString();
const resourcePath = resource.getPath();

// This code is almost identical to lbt/bundle/Builder.js
// Please try to update both places when making improvements or bug fixes

const sourceMapUrlMatch = resourceContent.match(sourceMappingUrlPattern);
if (sourceMapUrlMatch) {
const sourceMapUrl = sourceMapUrlMatch[1];
log.silly(`Found source map reference in content of resource ${resourcePath}: ${sourceMapUrl}`);

// Strip sourceMappingURL from resource code to be bundled
// It has no effect and might be cause for confusion
resource.setString(resourceContent.replace(sourceMappingUrlPattern, ""));

if (sourceMapUrl) {
if (sourceMapUrl.startsWith("data:")) {
// Data-URI indicates an inline source map
const expectedTypeAndEncoding = "data:application/json;charset=utf-8;base64,";
if (sourceMapUrl.startsWith(expectedTypeAndEncoding)) {
const base64Content = sourceMapUrl.slice(expectedTypeAndEncoding.length);
// 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: ${sourceMapUrl}. Expected it to start with ` +
`"data:application/json;charset=utf-8;base64,"`);
}
} else if (httpPattern.test(sourceMapUrl)) {
log.warn(`Source map reference in resource ${resourcePath} is an absolute URL. ` +
`Currently, only relative URLs are supported.`);
} else if (posixPath.isAbsolute(sourceMapUrl)) {
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), sourceMapUrl);

try {
const sourceMapContent = await readFile(sourceMapPath);
if (sourceMapContent) {
return sourceMapContent.toString();
} else {
throw new Error(`Not found: ${sourceMapPath}`);
}
} catch (e) {
// No input source map
log.warn(`Unable to read source map for resource ${resourcePath}: ${e.message}`);
}
}
} else {
const sourceMapFileCandidate = resourcePath + ".map";
log.silly(`Could not find a sourceMappingURL reference in content of resource ${resourcePath}. ` +
`Attempting to find a source map resource based on the module's path: ${sourceMapFileCandidate}`);
try {
const sourceMapContent = await readFile(sourceMapFileCandidate);
if (sourceMapContent) {
return sourceMapContent.toString();
}
} catch (e) {
// No input source map
log.silly(`Could not find a source map for resource ${resourcePath}: ${e.message}`);
}
}
}
}

/**
* @public
* @module @ui5/builder/processors/minifier
Expand All @@ -62,18 +135,29 @@ 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 "parseSourceMappingUrl" to true
* @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance.
* Required when using the <code>useWorkers</code> option
* @param {object} [parameters.options] Options
* @param {boolean} [parameters.options.parseSourceMappingUrl=false]
* Whether to use any existing source maps, either referenced in the resource or following the
* naming convention "<resource name>.map"
* @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]
* Whether to offload the minification task onto separate CPU threads. This often speeds up the build process
* @returns {Promise<module:@ui5/builder/processors/minifier~MinifierResult[]>}
* 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: {parseSourceMappingUrl = false, addSourceMappingUrl = true, useWorkers = false
} = {}}) {
let minify;
if (parseSourceMappingUrl && !fs) {
throw new Error(`Option 'parseSourceMappingUrl' requires parameter 'fs' to be provided`);
}

if (useWorkers) {
if (!taskUtil) {
// TaskUtil is required for worker support
Expand All @@ -86,20 +170,50 @@ 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
};
if (addSourceMappingUrl) {
sourceMapOptions.url = filename + ".map";
}
const dbgFilename = posixPath.basename(dbgPath);
let dbgSourceMapResource;
if (parseSourceMappingUrl) {
// 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 getSourceMap(resource, 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"
});
}
}

const dbgResource = await resource.clone();
dbgResource.setPath(dbgPath);
const code = await resource.getString();

if (dbgSourceMapResource) {
dbgResource.setString(code + `\n//# sourceMappingURL=${dbgFilename}.map`);
}

const result = await minify({
filename,
Expand All @@ -112,6 +226,6 @@ export default async function({resources, taskUtil, options: {addSourceMappingUr
path: resource.getPath() + ".map",
string: result.map
});
return {resource, dbgResource, sourceMapResource};
return {resource, dbgResource, sourceMapResource, dbgSourceMapResource};
}));
}
16 changes: 14 additions & 2 deletions lib/tasks/minify.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import minifier from "../processors/minifier.js";
import fsInterface from "@ui5/fs/fsInterface";

/**
* @public
Expand All @@ -25,26 +26,37 @@ export default async function({workspace, taskUtil, options: {pattern, omitSourc
const resources = await workspace.byGlob(pattern);
const processedResources = await minifier({
resources,
fs: fsInterface(workspace),
taskUtil,
options: {
addSourceMappingUrl: !omitSourceMapResources,

// TODO: Make this configurable, defaulting to false for the time being
parseSourceMappingUrl: true,

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);
taskUtil.setTag(sourceMapResource, taskUtil.STANDARD_TAGS.HasDebugVariant);
if (omitSourceMapResources) {
taskUtil.setTag(sourceMapResource, taskUtil.STANDARD_TAGS.OmitFromBuildResult);
if (dbgSourceMapResource) {
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)
]);
}));
}

0 comments on commit 9c31fda

Please sign in to comment.