fix(@angular-devkit/build-angular): update sourcemaps when rebasing Sass url() functions in esbuild builder

When using the experimental esbuild-based browser application builder with Sass and sourcemaps, the final
sourcemap for an input Sass stylesheet will now contain the original content for any `url` functions that
were rebased to support bundling. This required generating internal intermediate source maps for each imported
stylesheet that was modified with rebased URLs and then merging these intermediate source maps with the
final Sass generated source map. This process only occurs when stylesheet sourcemaps are enabled.
This commit is contained in:
Charles Lyding 2022-11-07 12:05:38 -05:00 committed by Alan Agius
parent 0cff3e09cd
commit f7ad20c465
5 changed files with 58 additions and 12 deletions

View File

@ -154,6 +154,7 @@ ts_library(
"@npm//less-loader",
"@npm//license-webpack-plugin",
"@npm//loader-utils",
"@npm//magic-string",
"@npm//mini-css-extract-plugin",
"@npm//minimatch",
"@npm//ng-packagr",

View File

@ -41,6 +41,7 @@
"less-loader": "11.1.0",
"license-webpack-plugin": "4.0.2",
"loader-utils": "3.2.0",
"magic-string": "0.26.7",
"mini-css-extract-plugin": "2.6.1",
"minimatch": "5.1.0",
"open": "8.4.0",

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import { RawSourceMap } from '@ampproject/remapping';
import MagicString from 'magic-string';
import { Dirent, readFileSync, readdirSync } from 'node:fs';
import { basename, dirname, extname, join, relative } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@ -31,8 +33,13 @@ const URL_REGEXP = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g;
abstract class UrlRebasingImporter implements Importer<'sync'> {
/**
* @param entryDirectory The directory of the entry stylesheet that was passed to the Sass compiler.
* @param rebaseSourceMaps When provided, rebased files will have an intermediate sourcemap added to the Map
* which can be used to generate a final sourcemap that contains original sources.
*/
constructor(private entryDirectory: string) {}
constructor(
private entryDirectory: string,
private rebaseSourceMaps?: Map<string, RawSourceMap>,
) {}
abstract canonicalize(url: string, options: { fromImport: boolean }): URL | null;
@ -46,6 +53,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
let match;
URL_REGEXP.lastIndex = 0;
let updatedContents;
while ((match = URL_REGEXP.exec(contents))) {
const originalUrl = match[2];
@ -60,10 +68,21 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
// https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
const rebasedUrl = './' + rebasedPath.replace(/\\/g, '/').replace(/[()\s'"]/g, '\\$&');
contents =
contents.slice(0, match.index) +
`url(${rebasedUrl})` +
contents.slice(match.index + match[0].length);
updatedContents ??= new MagicString(contents);
updatedContents.update(match.index, match.index + match[0].length, `url(${rebasedUrl})`);
}
if (updatedContents) {
contents = updatedContents.toString();
if (this.rebaseSourceMaps) {
// Generate an intermediate source map for the rebasing changes
const map = updatedContents.generateMap({
hires: true,
includeContent: true,
source: canonicalUrl.href,
});
this.rebaseSourceMaps.set(canonicalUrl.href, map as RawSourceMap);
}
}
}
@ -94,8 +113,12 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
* the URLs in the output of the Sass compiler reflect the final filesystem location of the output CSS file.
*/
export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
constructor(entryDirectory: string, private directoryCache = new Map<string, Dirent[]>()) {
super(entryDirectory);
constructor(
entryDirectory: string,
private directoryCache = new Map<string, Dirent[]>(),
rebaseSourceMaps?: Map<string, RawSourceMap>,
) {
super(entryDirectory, rebaseSourceMaps);
}
canonicalize(url: string, options: { fromImport: boolean }): URL | null {
@ -238,9 +261,10 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
constructor(
entryDirectory: string,
directoryCache: Map<string, Dirent[]>,
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
private finder: FileImporter<'sync'>['findFileUrl'],
) {
super(entryDirectory, directoryCache);
super(entryDirectory, directoryCache, rebaseSourceMaps);
}
override canonicalize(url: string, options: { fromImport: boolean }): URL | null {
@ -263,9 +287,10 @@ export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
constructor(
entryDirectory: string,
directoryCache: Map<string, Dirent[]>,
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
private loadPaths: Iterable<string>,
) {
super(entryDirectory, directoryCache);
super(entryDirectory, directoryCache, rebaseSourceMaps);
}
override canonicalize(url: string, options: { fromImport: boolean }): URL | null {

View File

@ -145,7 +145,7 @@ export class SassWorkerImplementation {
const callback: RenderCallback = (error, result) => {
if (error) {
const url = error?.span.url as string | undefined;
const url = error.span?.url as string | undefined;
if (url) {
error.span.url = pathToFileURL(url);
}

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping';
import { Dirent } from 'node:fs';
import { dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
@ -82,6 +83,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
| undefined;
try {
const directoryCache = new Map<string, Dirent[]>();
const rebaseSourceMaps = options.sourceMap ? new Map<string, RawSourceMap>() : undefined;
if (hasImporter) {
// When a custom importer function is present, the importer request must be proxied
// back to the main thread where it can be executed.
@ -105,6 +107,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
new ModuleUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
proxyImporter.findFileUrl,
),
)
@ -116,7 +119,12 @@ parentPort.on('message', (message: RenderRequestMessage) => {
options.importers ??= [];
options.importers.push(
sassBindWorkaround(
new LoadPathsUrlRebasingImporter(entryDirectory, directoryCache, options.loadPaths),
new LoadPathsUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
options.loadPaths,
),
),
);
options.loadPaths = undefined;
@ -125,7 +133,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
let relativeImporter;
if (rebase) {
relativeImporter = sassBindWorkaround(
new RelativeUrlRebasingImporter(entryDirectory, directoryCache),
new RelativeUrlRebasingImporter(entryDirectory, directoryCache, rebaseSourceMaps),
);
}
@ -151,6 +159,17 @@ parentPort.on('message', (message: RenderRequestMessage) => {
: undefined,
});
if (result.sourceMap && rebaseSourceMaps?.size) {
// Merge the intermediate rebasing source maps into the final Sass generated source map.
// Casting is required due to small but compatible differences in typings between the packages.
result.sourceMap = mergeSourceMaps(
result.sourceMap as unknown as RawSourceMap,
// To prevent an infinite lookup loop, skip getting the source when the rebasing source map
// is referencing its original self.
(file, context) => (file !== context.importer ? rebaseSourceMaps.get(file) : null),
) as unknown as typeof result.sourceMap;
}
parentPort.postMessage({
id,
warnings,