From f7ad20c4652956f9f47ac200027d8c144fed6d6d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 7 Nov 2022 12:05:38 -0500 Subject: [PATCH] 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. --- .../angular_devkit/build_angular/BUILD.bazel | 1 + .../angular_devkit/build_angular/package.json | 1 + .../src/sass/rebasing-importer.ts | 43 +++++++++++++++---- .../build_angular/src/sass/sass-service.ts | 2 +- .../build_angular/src/sass/worker.ts | 23 +++++++++- 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index 6ad91a71dd..966d0346bb 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 7ded8a0f2c..e1c94cce18 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 4e4943bbab..976b59f5c7 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 1db3b50709..abdf6e76aa 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 3723f91c2c..160ccf3f89 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,