Skip to content

Commit 8f8e02c

Browse files
committed
fix(@angular-devkit/build-angular): support Yarn PNP resolution in modern SASS API
This change add a Sass File importer that uses Webpack resolvers to better support scenarios when node packages are not stored in node_modules, such as Yarn PNP.
1 parent 15355a8 commit 8f8e02c

File tree

1 file changed

+50
-15
lines changed
  • packages/angular_devkit/build_angular/src/webpack/configs

1 file changed

+50
-15
lines changed

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

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import * as fs from 'fs';
1010
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
1111
import * as path from 'path';
12-
import { Configuration, RuleSetUseItem } from 'webpack';
12+
import type { FileImporter } from 'sass';
13+
import { pathToFileURL } from 'url';
14+
import type { Configuration, LoaderContext, RuleSetUseItem } from 'webpack';
1315
import { StyleElement } from '../../builders/browser/schema';
1416
import { SassWorkerImplementation } from '../../sass/sass-service';
1517
import { SassLegacyWorkerImplementation } from '../../sass/sass-service-legacy';
@@ -267,11 +269,11 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
267269
loader: require.resolve('sass-loader'),
268270
options: getSassLoaderOptions(
269271
root,
270-
projectRoot,
271272
sassImplementation,
272273
includePaths,
273274
false,
274275
!buildOptions.verbose,
276+
!!buildOptions.preserveSymlinks,
275277
),
276278
},
277279
],
@@ -289,11 +291,11 @@ export function getStylesConfig(wco: WebpackConfigOptions): Configuration {
289291
loader: require.resolve('sass-loader'),
290292
options: getSassLoaderOptions(
291293
root,
292-
projectRoot,
293294
sassImplementation,
294295
includePaths,
295296
true,
296297
!buildOptions.verbose,
298+
!!buildOptions.preserveSymlinks,
297299
),
298300
},
299301
],
@@ -376,29 +378,23 @@ function getTailwindConfigPath({ projectRoot, root }: WebpackConfigOptions): str
376378

377379
function getSassLoaderOptions(
378380
root: string,
379-
projectRoot: string,
380381
implementation: SassWorkerImplementation | SassLegacyWorkerImplementation,
381382
includePaths: string[],
382383
indentedSyntax: boolean,
383384
verbose: boolean,
385+
preserveSymlinks: boolean,
384386
): Record<string, unknown> {
385387
return implementation instanceof SassWorkerImplementation
386388
? {
387389
sourceMap: true,
388390
api: 'modern',
389391
implementation,
390-
// Webpack importer is only implemented in the legacy API.
392+
// Webpack importer is only implemented in the legacy API and we have our own custom Webpack importer.
391393
// See: https://github.com/webpack-contrib/sass-loader/blob/997f3eb41d86dd00d5fa49c395a1aeb41573108c/src/utils.js#L642-L651
392394
webpackImporter: false,
393-
sassOptions: {
394-
loadPaths: [
395-
...includePaths,
396-
// Needed to resolve node packages and retain the same behaviour of with the legacy API as sass-loader resolves
397-
// scss also from the cwd and project root.
398-
// See: https://github.com/webpack-contrib/sass-loader/blob/997f3eb41d86dd00d5fa49c395a1aeb41573108c/src/utils.js#L307
399-
projectRoot,
400-
path.join(root, 'node_modules'),
401-
],
395+
sassOptions: (loaderContext: LoaderContext<{}>) => ({
396+
importers: [getSassResolutionImporter(loaderContext, root, preserveSymlinks)],
397+
loadPaths: includePaths,
402398
// Use expanded as otherwise sass will remove comments that are needed for autoprefixer
403399
// Ex: /* autoprefixer grid: autoplace */
404400
// See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70
@@ -407,7 +403,7 @@ function getSassLoaderOptions(
407403
quietDeps: !verbose,
408404
verbose,
409405
syntax: indentedSyntax ? 'indented' : 'scss',
410-
},
406+
}),
411407
}
412408
: {
413409
sourceMap: true,
@@ -439,3 +435,42 @@ function getSassLoaderOptions(
439435
},
440436
};
441437
}
438+
439+
function getSassResolutionImporter(
440+
loaderContext: LoaderContext<{}>,
441+
root: string,
442+
preserveSymlinks: boolean,
443+
): FileImporter<'async'> {
444+
const commonResolverOptions: Parameters<typeof loaderContext['getResolve']>[0] = {
445+
conditionNames: ['sass', 'style'],
446+
mainFields: ['sass', 'style', 'main', '...'],
447+
extensions: ['.scss', '.sass', '.css'],
448+
restrictions: [/\.((sa|sc|c)ss)$/i],
449+
preferRelative: true,
450+
symlinks: !preserveSymlinks,
451+
};
452+
453+
// Sass also supports import-only files. If you name a file <name>.import.scss, it will only be loaded for imports, not for @uses.
454+
// See: https://sass-lang.com/documentation/at-rules/import#import-only-files
455+
const resolveImport = loaderContext.getResolve({
456+
...commonResolverOptions,
457+
dependencyType: 'sass-import',
458+
mainFiles: ['_index.import', '_index', 'index.import', 'index', '...'],
459+
});
460+
461+
const resolveModule = loaderContext.getResolve({
462+
...commonResolverOptions,
463+
dependencyType: 'sass-module',
464+
mainFiles: ['_index', 'index', '...'],
465+
});
466+
467+
return {
468+
findFileUrl: (url, { fromImport }): Promise<URL | null> => {
469+
const resolve = fromImport ? resolveImport : resolveModule;
470+
471+
return resolve(root, url)
472+
.then((file) => pathToFileURL(file))
473+
.catch(() => null);
474+
},
475+
};
476+
}

0 commit comments

Comments
 (0)