Skip to content

Commit f7ad20c

Browse files
clydinalan-agius4
authored andcommitted
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.
1 parent 0cff3e0 commit f7ad20c

File tree

5 files changed

+58
-12
lines changed

5 files changed

+58
-12
lines changed

packages/angular_devkit/build_angular/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ ts_library(
154154
"@npm//less-loader",
155155
"@npm//license-webpack-plugin",
156156
"@npm//loader-utils",
157+
"@npm//magic-string",
157158
"@npm//mini-css-extract-plugin",
158159
"@npm//minimatch",
159160
"@npm//ng-packagr",

packages/angular_devkit/build_angular/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"less-loader": "11.1.0",
4242
"license-webpack-plugin": "4.0.2",
4343
"loader-utils": "3.2.0",
44+
"magic-string": "0.26.7",
4445
"mini-css-extract-plugin": "2.6.1",
4546
"minimatch": "5.1.0",
4647
"open": "8.4.0",

packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts

+34-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import { RawSourceMap } from '@ampproject/remapping';
10+
import MagicString from 'magic-string';
911
import { Dirent, readFileSync, readdirSync } from 'node:fs';
1012
import { basename, dirname, extname, join, relative } from 'node:path';
1113
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -31,8 +33,13 @@ const URL_REGEXP = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g;
3133
abstract class UrlRebasingImporter implements Importer<'sync'> {
3234
/**
3335
* @param entryDirectory The directory of the entry stylesheet that was passed to the Sass compiler.
36+
* @param rebaseSourceMaps When provided, rebased files will have an intermediate sourcemap added to the Map
37+
* which can be used to generate a final sourcemap that contains original sources.
3438
*/
35-
constructor(private entryDirectory: string) {}
39+
constructor(
40+
private entryDirectory: string,
41+
private rebaseSourceMaps?: Map<string, RawSourceMap>,
42+
) {}
3643

3744
abstract canonicalize(url: string, options: { fromImport: boolean }): URL | null;
3845

@@ -46,6 +53,7 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
4653

4754
let match;
4855
URL_REGEXP.lastIndex = 0;
56+
let updatedContents;
4957
while ((match = URL_REGEXP.exec(contents))) {
5058
const originalUrl = match[2];
5159

@@ -60,10 +68,21 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
6068
// https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
6169
const rebasedUrl = './' + rebasedPath.replace(/\\/g, '/').replace(/[()\s'"]/g, '\\$&');
6270

63-
contents =
64-
contents.slice(0, match.index) +
65-
`url(${rebasedUrl})` +
66-
contents.slice(match.index + match[0].length);
71+
updatedContents ??= new MagicString(contents);
72+
updatedContents.update(match.index, match.index + match[0].length, `url(${rebasedUrl})`);
73+
}
74+
75+
if (updatedContents) {
76+
contents = updatedContents.toString();
77+
if (this.rebaseSourceMaps) {
78+
// Generate an intermediate source map for the rebasing changes
79+
const map = updatedContents.generateMap({
80+
hires: true,
81+
includeContent: true,
82+
source: canonicalUrl.href,
83+
});
84+
this.rebaseSourceMaps.set(canonicalUrl.href, map as RawSourceMap);
85+
}
6786
}
6887
}
6988

@@ -94,8 +113,12 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
94113
* the URLs in the output of the Sass compiler reflect the final filesystem location of the output CSS file.
95114
*/
96115
export class RelativeUrlRebasingImporter extends UrlRebasingImporter {
97-
constructor(entryDirectory: string, private directoryCache = new Map<string, Dirent[]>()) {
98-
super(entryDirectory);
116+
constructor(
117+
entryDirectory: string,
118+
private directoryCache = new Map<string, Dirent[]>(),
119+
rebaseSourceMaps?: Map<string, RawSourceMap>,
120+
) {
121+
super(entryDirectory, rebaseSourceMaps);
99122
}
100123

101124
canonicalize(url: string, options: { fromImport: boolean }): URL | null {
@@ -238,9 +261,10 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
238261
constructor(
239262
entryDirectory: string,
240263
directoryCache: Map<string, Dirent[]>,
264+
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
241265
private finder: FileImporter<'sync'>['findFileUrl'],
242266
) {
243-
super(entryDirectory, directoryCache);
267+
super(entryDirectory, directoryCache, rebaseSourceMaps);
244268
}
245269

246270
override canonicalize(url: string, options: { fromImport: boolean }): URL | null {
@@ -263,9 +287,10 @@ export class LoadPathsUrlRebasingImporter extends RelativeUrlRebasingImporter {
263287
constructor(
264288
entryDirectory: string,
265289
directoryCache: Map<string, Dirent[]>,
290+
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
266291
private loadPaths: Iterable<string>,
267292
) {
268-
super(entryDirectory, directoryCache);
293+
super(entryDirectory, directoryCache, rebaseSourceMaps);
269294
}
270295

271296
override canonicalize(url: string, options: { fromImport: boolean }): URL | null {

packages/angular_devkit/build_angular/src/sass/sass-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class SassWorkerImplementation {
145145

146146
const callback: RenderCallback = (error, result) => {
147147
if (error) {
148-
const url = error?.span.url as string | undefined;
148+
const url = error.span?.url as string | undefined;
149149
if (url) {
150150
error.span.url = pathToFileURL(url);
151151
}

packages/angular_devkit/build_angular/src/sass/worker.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import mergeSourceMaps, { RawSourceMap } from '@ampproject/remapping';
910
import { Dirent } from 'node:fs';
1011
import { dirname } from 'node:path';
1112
import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -82,6 +83,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
8283
| undefined;
8384
try {
8485
const directoryCache = new Map<string, Dirent[]>();
86+
const rebaseSourceMaps = options.sourceMap ? new Map<string, RawSourceMap>() : undefined;
8587
if (hasImporter) {
8688
// When a custom importer function is present, the importer request must be proxied
8789
// back to the main thread where it can be executed.
@@ -105,6 +107,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
105107
new ModuleUrlRebasingImporter(
106108
entryDirectory,
107109
directoryCache,
110+
rebaseSourceMaps,
108111
proxyImporter.findFileUrl,
109112
),
110113
)
@@ -116,7 +119,12 @@ parentPort.on('message', (message: RenderRequestMessage) => {
116119
options.importers ??= [];
117120
options.importers.push(
118121
sassBindWorkaround(
119-
new LoadPathsUrlRebasingImporter(entryDirectory, directoryCache, options.loadPaths),
122+
new LoadPathsUrlRebasingImporter(
123+
entryDirectory,
124+
directoryCache,
125+
rebaseSourceMaps,
126+
options.loadPaths,
127+
),
120128
),
121129
);
122130
options.loadPaths = undefined;
@@ -125,7 +133,7 @@ parentPort.on('message', (message: RenderRequestMessage) => {
125133
let relativeImporter;
126134
if (rebase) {
127135
relativeImporter = sassBindWorkaround(
128-
new RelativeUrlRebasingImporter(entryDirectory, directoryCache),
136+
new RelativeUrlRebasingImporter(entryDirectory, directoryCache, rebaseSourceMaps),
129137
);
130138
}
131139

@@ -151,6 +159,17 @@ parentPort.on('message', (message: RenderRequestMessage) => {
151159
: undefined,
152160
});
153161

162+
if (result.sourceMap && rebaseSourceMaps?.size) {
163+
// Merge the intermediate rebasing source maps into the final Sass generated source map.
164+
// Casting is required due to small but compatible differences in typings between the packages.
165+
result.sourceMap = mergeSourceMaps(
166+
result.sourceMap as unknown as RawSourceMap,
167+
// To prevent an infinite lookup loop, skip getting the source when the rebasing source map
168+
// is referencing its original self.
169+
(file, context) => (file !== context.importer ? rebaseSourceMaps.get(file) : null),
170+
) as unknown as typeof result.sourceMap;
171+
}
172+
154173
parentPort.postMessage({
155174
id,
156175
warnings,

0 commit comments

Comments
 (0)