diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index 6ad91a71dd2a..966d0346bb8d 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -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", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 7ded8a0f2c67..e1c94cce182a 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -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", diff --git a/packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts b/packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts index 4e4943bbabbc..976b59f5c782 100644 --- a/packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts +++ b/packages/angular_devkit/build_angular/src/sass/rebasing-importer.ts @@ -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, + ) {} 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()) { - super(entryDirectory); + constructor( + entryDirectory: string, + private directoryCache = new Map(), + rebaseSourceMaps?: Map, + ) { + 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, + rebaseSourceMaps: Map | 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, + rebaseSourceMaps: Map | undefined, private loadPaths: Iterable, ) { - super(entryDirectory, directoryCache); + super(entryDirectory, directoryCache, rebaseSourceMaps); } override canonicalize(url: string, options: { fromImport: boolean }): URL | null { diff --git a/packages/angular_devkit/build_angular/src/sass/sass-service.ts b/packages/angular_devkit/build_angular/src/sass/sass-service.ts index 1db3b507098c..abdf6e76aa68 100644 --- a/packages/angular_devkit/build_angular/src/sass/sass-service.ts +++ b/packages/angular_devkit/build_angular/src/sass/sass-service.ts @@ -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); } diff --git a/packages/angular_devkit/build_angular/src/sass/worker.ts b/packages/angular_devkit/build_angular/src/sass/worker.ts index 3723f91c2c49..160ccf3f89b0 100644 --- a/packages/angular_devkit/build_angular/src/sass/worker.ts +++ b/packages/angular_devkit/build_angular/src/sass/worker.ts @@ -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(); + const rebaseSourceMaps = options.sourceMap ? new Map() : 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,