Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): update sourcemaps when rebasing S…
Browse files Browse the repository at this point in the history
…ass 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.

(cherry picked from commit f7ad20c)
  • Loading branch information
clydin authored and alan-agius4 committed Nov 8, 2022
1 parent 2115ac1 commit 0d62157
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/BUILD.bazel
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/package.json
Expand Up @@ -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",
Expand Down
Expand Up @@ -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';
Expand All @@ -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;

Expand All @@ -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];

Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Expand Up @@ -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);
}
Expand Down
23 changes: 21 additions & 2 deletions packages/angular_devkit/build_angular/src/sass/worker.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand All @@ -105,6 +107,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
new ModuleUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
proxyImporter.findFileUrl,
),
)
Expand All @@ -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;
Expand All @@ -125,7 +133,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
let relativeImporter;
if (rebase) {
relativeImporter = sassBindWorkaround(
new RelativeUrlRebasingImporter(entryDirectory, directoryCache),
new RelativeUrlRebasingImporter(entryDirectory, directoryCache, rebaseSourceMaps),
);
}

Expand All @@ -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,
Expand Down

0 comments on commit 0d62157

Please sign in to comment.