Skip to content

Commit 2105964

Browse files
committed
fix(@angular-devkit/build-angular): resolve transitive dependencies in Sass when using Yarn PNP
Enhanced resolver is unable to resolve transitive dependencies in Sass when using Yarn PNP. The main reason for this is that Sass doesn't provide context on which file is requesting the module. See: sass/sass#3247. As a workaround for this we store previously resolved paths and when a new request comes in we try to resolve this from the previously resolved files if we are unable to resolve the request from the workspace root. (cherry picked from commit c49f1ee)
1 parent 22955f2 commit 2105964

File tree

2 files changed

+69
-24
lines changed

2 files changed

+69
-24
lines changed

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { join } from 'path';
9+
import { dirname, join } from 'node:path';
10+
import { fileURLToPath, pathToFileURL } from 'node:url';
11+
import { MessageChannel, Worker } from 'node:worker_threads';
1012
import {
1113
CompileResult,
1214
Exception,
@@ -15,8 +17,6 @@ import {
1517
StringOptionsWithImporter,
1618
StringOptionsWithoutImporter,
1719
} from 'sass';
18-
import { fileURLToPath, pathToFileURL } from 'url';
19-
import { MessageChannel, Worker } from 'worker_threads';
2020
import { maxWorkers } from '../utils/environment-options';
2121

2222
/**
@@ -31,6 +31,16 @@ type RenderCallback = (error?: Exception, result?: CompileResult) => void;
3131

3232
type FileImporterOptions = Parameters<FileImporter['findFileUrl']>[1];
3333

34+
export interface FileImporterWithRequestContextOptions extends FileImporterOptions {
35+
/**
36+
* This is a custom option and is required as SASS does not provide context from which the file is being resolved.
37+
* This breaks Yarn PNP as transitive deps cannot be resolved from the workspace root.
38+
*
39+
* Workaround until https://github.com/sass/sass/issues/3247 is addressed.
40+
*/
41+
previousResolvedModules?: Set<string>;
42+
}
43+
3444
/**
3545
* An object containing the contextual information for a specific render request.
3646
*/
@@ -39,6 +49,7 @@ interface RenderRequest {
3949
workerIndex: number;
4050
callback: RenderCallback;
4151
importers?: Importers[];
52+
previousResolvedModules?: Set<string>;
4253
}
4354

4455
/**
@@ -212,8 +223,16 @@ export class SassWorkerImplementation {
212223
return;
213224
}
214225

215-
this.processImporters(request.importers, url, options)
226+
this.processImporters(request.importers, url, {
227+
...options,
228+
previousResolvedModules: request.previousResolvedModules,
229+
})
216230
.then((result) => {
231+
if (result) {
232+
request.previousResolvedModules ??= new Set();
233+
request.previousResolvedModules.add(dirname(result));
234+
}
235+
217236
mainImporterPort.postMessage(result);
218237
})
219238
.catch((error) => {
@@ -234,7 +253,7 @@ export class SassWorkerImplementation {
234253
private async processImporters(
235254
importers: Iterable<Importers>,
236255
url: string,
237-
options: FileImporterOptions,
256+
options: FileImporterWithRequestContextOptions,
238257
): Promise<string | null> {
239258
for (const importer of importers) {
240259
if (this.isImporter(importer)) {

packages/angular_devkit/build_angular/src/webpack/configs/styles.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import * as fs from 'fs';
109
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
11-
import * as path from 'path';
10+
import * as fs from 'node:fs';
11+
import * as path from 'node:path';
12+
import { pathToFileURL } from 'node:url';
1213
import type { FileImporter } from 'sass';
13-
import { pathToFileURL } from 'url';
1414
import type { Configuration, LoaderContext, RuleSetUseItem } from 'webpack';
15-
import { SassWorkerImplementation } from '../../sass/sass-service';
15+
import {
16+
FileImporterWithRequestContextOptions,
17+
SassWorkerImplementation,
18+
} from '../../sass/sass-service';
1619
import { SassLegacyWorkerImplementation } from '../../sass/sass-service-legacy';
1720
import { WebpackConfigOptions } from '../../utils/build-options';
1821
import { useLegacySass } from '../../utils/environment-options';
@@ -413,30 +416,53 @@ function getSassResolutionImporter(
413416
});
414417

415418
return {
416-
findFileUrl: async (url, { fromImport }): Promise<URL | null> => {
419+
findFileUrl: async (
420+
url,
421+
{ fromImport, previousResolvedModules }: FileImporterWithRequestContextOptions,
422+
): Promise<URL | null> => {
417423
if (url.charAt(0) === '.') {
418424
// Let Sass handle relative imports.
419425
return null;
420426
}
421427

422-
let file: string | undefined;
423428
const resolve = fromImport ? resolveImport : resolveModule;
424-
425-
try {
426-
file = await resolve(root, url);
427-
} catch {
428-
// Try to resolve a partial file
429-
// @use '@material/button/button' as mdc-button;
430-
// `@material/button/button` -> `@material/button/_button`
431-
const lastSlashIndex = url.lastIndexOf('/');
432-
const underscoreIndex = lastSlashIndex + 1;
433-
if (underscoreIndex > 0 && url.charAt(underscoreIndex) !== '_') {
434-
const partialFileUrl = `${url.slice(0, underscoreIndex)}_${url.slice(underscoreIndex)}`;
435-
file = await resolve(root, partialFileUrl).catch(() => undefined);
429+
// Try to resolve from root of workspace
430+
let result = await tryResolve(resolve, root, url);
431+
432+
// Try to resolve from previously resolved modules.
433+
if (!result && previousResolvedModules) {
434+
for (const path of previousResolvedModules) {
435+
result = await tryResolve(resolve, path, url);
436+
if (result) {
437+
break;
438+
}
436439
}
437440
}
438441

439-
return file ? pathToFileURL(file) : null;
442+
return result ? pathToFileURL(result) : null;
440443
},
441444
};
442445
}
446+
447+
async function tryResolve(
448+
resolve: ReturnType<LoaderContext<{}>['getResolve']>,
449+
root: string,
450+
url: string,
451+
): Promise<string | undefined> {
452+
try {
453+
return await resolve(root, url);
454+
} catch {
455+
// Try to resolve a partial file
456+
// @use '@material/button/button' as mdc-button;
457+
// `@material/button/button` -> `@material/button/_button`
458+
const lastSlashIndex = url.lastIndexOf('/');
459+
const underscoreIndex = lastSlashIndex + 1;
460+
if (underscoreIndex > 0 && url.charAt(underscoreIndex) !== '_') {
461+
const partialFileUrl = `${url.slice(0, underscoreIndex)}_${url.slice(underscoreIndex)}`;
462+
463+
return resolve(root, partialFileUrl).catch(() => undefined);
464+
}
465+
}
466+
467+
return undefined;
468+
}

0 commit comments

Comments
 (0)