mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 11:44:05 +08:00
refactor(@angular-devkit/build-angular): remove Sass module resolve workarounds
The recent version of the Sass compiler (`dart-sass@1.68.0`) now provides additional information within an importer that allows more accurate resolution of node modules packages without several existing workarounds. Previously, the Sass files needed to be pre-processed to extract all `@import` and `@use` paths so that information regarding the containing Sass file could be used to fully resolve the paths. The Sass compiler now provides this information directly.
This commit is contained in:
parent
babe4674f8
commit
0862a38441
@ -9,11 +9,8 @@
|
|||||||
import type { OnLoadResult, PartialMessage, ResolveResult } from 'esbuild';
|
import type { OnLoadResult, PartialMessage, ResolveResult } from 'esbuild';
|
||||||
import { dirname, join, relative } from 'node:path';
|
import { dirname, join, relative } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import type { CompileResult, Exception, Syntax } from 'sass';
|
import type { CanonicalizeContext, CompileResult, Exception, Syntax } from 'sass';
|
||||||
import type {
|
import type { SassWorkerImplementation } from '../../sass/sass-service';
|
||||||
FileImporterWithRequestContextOptions,
|
|
||||||
SassWorkerImplementation,
|
|
||||||
} from '../../sass/sass-service';
|
|
||||||
import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory';
|
import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory';
|
||||||
|
|
||||||
let sassWorkerPool: SassWorkerImplementation | undefined;
|
let sassWorkerPool: SassWorkerImplementation | undefined;
|
||||||
@ -39,30 +36,16 @@ export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
|
|||||||
fileFilter: /\.s[ac]ss$/,
|
fileFilter: /\.s[ac]ss$/,
|
||||||
process(data, file, format, options, build) {
|
process(data, file, format, options, build) {
|
||||||
const syntax = format === 'sass' ? 'indented' : 'scss';
|
const syntax = format === 'sass' ? 'indented' : 'scss';
|
||||||
const resolveUrl = async (url: string, options: FileImporterWithRequestContextOptions) => {
|
const resolveUrl = async (url: string, options: CanonicalizeContext) => {
|
||||||
let result = await build.resolve(url, {
|
let resolveDir = build.initialOptions.absWorkingDir;
|
||||||
kind: 'import-rule',
|
if (options.containingUrl) {
|
||||||
// Use the provided resolve directory from the custom Sass service if available
|
resolveDir = dirname(fileURLToPath(options.containingUrl));
|
||||||
resolveDir: options.resolveDir ?? build.initialOptions.absWorkingDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If a resolve directory is provided, no additional speculative resolutions are required
|
|
||||||
if (options.resolveDir) {
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround to support Yarn PnP and pnpm without access to the importer file from Sass
|
const result = await build.resolve(url, {
|
||||||
if (!result.path && options.previousResolvedModules?.size) {
|
|
||||||
for (const previous of options.previousResolvedModules) {
|
|
||||||
result = await build.resolve(url, {
|
|
||||||
kind: 'import-rule',
|
kind: 'import-rule',
|
||||||
resolveDir: previous,
|
resolveDir,
|
||||||
});
|
});
|
||||||
if (result.path) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@ -103,10 +86,7 @@ async function compileString(
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
syntax: Syntax,
|
syntax: Syntax,
|
||||||
options: StylesheetPluginOptions,
|
options: StylesheetPluginOptions,
|
||||||
resolveUrl: (
|
resolveUrl: (url: string, options: CanonicalizeContext) => Promise<ResolveResult>,
|
||||||
url: string,
|
|
||||||
options: FileImporterWithRequestContextOptions,
|
|
||||||
) => Promise<ResolveResult>,
|
|
||||||
): Promise<OnLoadResult> {
|
): Promise<OnLoadResult> {
|
||||||
// Lazily load Sass when a Sass file is found
|
// Lazily load Sass when a Sass file is found
|
||||||
if (sassWorkerPool === undefined) {
|
if (sassWorkerPool === undefined) {
|
||||||
@ -139,7 +119,7 @@ async function compileString(
|
|||||||
quietDeps: true,
|
quietDeps: true,
|
||||||
importers: [
|
importers: [
|
||||||
{
|
{
|
||||||
findFileUrl: (url, options: FileImporterWithRequestContextOptions) =>
|
findFileUrl: (url, options) =>
|
||||||
resolutionCache.getOrCreate(url, async () => {
|
resolutionCache.getOrCreate(url, async () => {
|
||||||
const result = await resolveUrl(url, options);
|
const result = await resolveUrl(url, options);
|
||||||
if (result.path) {
|
if (result.path) {
|
||||||
|
@ -12,7 +12,7 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|||||||
import { basename, dirname, extname, join, relative } from 'node:path';
|
import { basename, dirname, extname, join, relative } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass';
|
import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass';
|
||||||
import { findImports, findUrls } from './lexer';
|
import { findUrls } from './lexer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A preprocessed cache entry for the files and directories within a previously searched
|
* A preprocessed cache entry for the files and directories within a previously searched
|
||||||
@ -23,45 +23,6 @@ export interface DirectoryEntry {
|
|||||||
directories: Set<string>;
|
directories: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A prefix that is added to import and use directive specifiers that should be resolved
|
|
||||||
* as modules and that will contain added resolve directory information.
|
|
||||||
*
|
|
||||||
* This functionality is used to workaround the Sass limitation that it does not provide the
|
|
||||||
* importer file to custom resolution plugins.
|
|
||||||
*/
|
|
||||||
const MODULE_RESOLUTION_PREFIX = '__NG_PACKAGE__';
|
|
||||||
|
|
||||||
function packModuleSpecifier(specifier: string, resolveDir: string): string {
|
|
||||||
const packed =
|
|
||||||
MODULE_RESOLUTION_PREFIX +
|
|
||||||
';' +
|
|
||||||
// Encode the resolve directory to prevent unsupported characters from being present when
|
|
||||||
// Sass processes the URL. This is important on Windows which can contain drive letters
|
|
||||||
// and colons which would otherwise be interpreted as a URL scheme.
|
|
||||||
encodeURIComponent(resolveDir) +
|
|
||||||
';' +
|
|
||||||
// Escape characters instead of encoding to provide more friendly not found error messages.
|
|
||||||
// Unescaping is automatically handled by Sass.
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/url#syntax
|
|
||||||
specifier.replace(/[()\s'"]/g, '\\$&');
|
|
||||||
|
|
||||||
return packed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function unpackModuleSpecifier(specifier: string): { specifier: string; resolveDir?: string } {
|
|
||||||
if (!specifier.startsWith(`${MODULE_RESOLUTION_PREFIX};`)) {
|
|
||||||
return { specifier };
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = specifier.split(';', 3);
|
|
||||||
|
|
||||||
return {
|
|
||||||
specifier: values[2],
|
|
||||||
resolveDir: decodeURIComponent(values[1]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Sass Importer base class that provides the load logic to rebase all `url()` functions
|
* A Sass Importer base class that provides the load logic to rebase all `url()` functions
|
||||||
* within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
|
* within a stylesheet. The rebasing will ensure that the URLs in the output of the Sass compiler
|
||||||
@ -114,27 +75,6 @@ abstract class UrlRebasingImporter implements Importer<'sync'> {
|
|||||||
updatedContents.update(start, end, rebasedUrl);
|
updatedContents.update(start, end, rebasedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add resolution directory information to module specifiers to facilitate resolution
|
|
||||||
for (const { start, end, specifier } of findImports(contents)) {
|
|
||||||
// Currently only provide directory information for known/common packages:
|
|
||||||
// * `@material/`
|
|
||||||
// * `@angular/`
|
|
||||||
//
|
|
||||||
// Comprehensive pre-resolution support may be added in the future. This is complicated by CSS/Sass not
|
|
||||||
// requiring a `./` or `../` prefix to signify relative paths. A bare specifier could be either relative
|
|
||||||
// or a module specifier. To differentiate, a relative resolution would need to be attempted first.
|
|
||||||
if (!specifier.startsWith('@angular/') && !specifier.startsWith('@material/')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContents ??= new MagicString(contents);
|
|
||||||
updatedContents.update(
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
`"${packModuleSpecifier(specifier, stylesheetDirectory)}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedContents) {
|
if (updatedContents) {
|
||||||
contents = updatedContents.toString();
|
contents = updatedContents.toString();
|
||||||
if (this.rebaseSourceMaps) {
|
if (this.rebaseSourceMaps) {
|
||||||
@ -348,10 +288,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
|
|||||||
entryDirectory: string,
|
entryDirectory: string,
|
||||||
directoryCache: Map<string, DirectoryEntry>,
|
directoryCache: Map<string, DirectoryEntry>,
|
||||||
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
|
rebaseSourceMaps: Map<string, RawSourceMap> | undefined,
|
||||||
private finder: (
|
private finder: (specifier: string, options: CanonicalizeContext) => URL | null,
|
||||||
specifier: string,
|
|
||||||
options: CanonicalizeContext & { resolveDir?: string },
|
|
||||||
) => URL | null,
|
|
||||||
) {
|
) {
|
||||||
super(entryDirectory, directoryCache, rebaseSourceMaps);
|
super(entryDirectory, directoryCache, rebaseSourceMaps);
|
||||||
}
|
}
|
||||||
@ -361,9 +298,7 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
|
|||||||
return super.canonicalize(url, options);
|
return super.canonicalize(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { specifier, resolveDir } = unpackModuleSpecifier(url);
|
let result = this.finder(url, options);
|
||||||
|
|
||||||
let result = this.finder(specifier, { ...options, resolveDir });
|
|
||||||
result &&= super.canonicalize(result.href, options);
|
result &&= super.canonicalize(result.href, options);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -6,10 +6,11 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { dirname, join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { MessageChannel, Worker } from 'node:worker_threads';
|
import { MessageChannel, Worker } from 'node:worker_threads';
|
||||||
import {
|
import {
|
||||||
|
CanonicalizeContext,
|
||||||
CompileResult,
|
CompileResult,
|
||||||
Exception,
|
Exception,
|
||||||
FileImporter,
|
FileImporter,
|
||||||
@ -31,24 +32,6 @@ const MAX_RENDER_WORKERS = maxWorkers;
|
|||||||
*/
|
*/
|
||||||
type RenderCallback = (error?: Exception, result?: CompileResult) => void;
|
type RenderCallback = (error?: Exception, result?: CompileResult) => void;
|
||||||
|
|
||||||
type FileImporterOptions = Parameters<FileImporter['findFileUrl']>[1];
|
|
||||||
|
|
||||||
export interface FileImporterWithRequestContextOptions extends FileImporterOptions {
|
|
||||||
/**
|
|
||||||
* This is a custom option and is required as SASS does not provide context from which the file is being resolved.
|
|
||||||
* This breaks Yarn PNP as transitive deps cannot be resolved from the workspace root.
|
|
||||||
*
|
|
||||||
* Workaround until https://github.com/sass/sass/issues/3247 is addressed.
|
|
||||||
*/
|
|
||||||
previousResolvedModules?: Set<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base directory to use when resolving the request.
|
|
||||||
* This value is only set if using the rebasing importers.
|
|
||||||
*/
|
|
||||||
resolveDir?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object containing the contextual information for a specific render request.
|
* An object containing the contextual information for a specific render request.
|
||||||
*/
|
*/
|
||||||
@ -58,7 +41,6 @@ interface RenderRequest {
|
|||||||
callback: RenderCallback;
|
callback: RenderCallback;
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
importers?: Importers[];
|
importers?: Importers[];
|
||||||
previousResolvedModules?: Set<string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,7 +226,7 @@ export class SassWorkerImplementation {
|
|||||||
|
|
||||||
mainImporterPort.on(
|
mainImporterPort.on(
|
||||||
'message',
|
'message',
|
||||||
({ id, url, options }: { id: number; url: string; options: FileImporterOptions }) => {
|
({ id, url, options }: { id: number; url: string; options: CanonicalizeContext }) => {
|
||||||
const request = this.requests.get(id);
|
const request = this.requests.get(id);
|
||||||
if (!request?.importers) {
|
if (!request?.importers) {
|
||||||
mainImporterPort.postMessage(null);
|
mainImporterPort.postMessage(null);
|
||||||
@ -256,14 +238,12 @@ export class SassWorkerImplementation {
|
|||||||
|
|
||||||
this.processImporters(request.importers, url, {
|
this.processImporters(request.importers, url, {
|
||||||
...options,
|
...options,
|
||||||
previousResolvedModules: request.previousResolvedModules,
|
// URL is not serializable so in the worker we convert to string and here back to URL.
|
||||||
|
containingUrl: options.containingUrl
|
||||||
|
? pathToFileURL(options.containingUrl as unknown as string)
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result) {
|
|
||||||
request.previousResolvedModules ??= new Set();
|
|
||||||
request.previousResolvedModules.add(dirname(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
mainImporterPort.postMessage(result);
|
mainImporterPort.postMessage(result);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -284,7 +264,7 @@ export class SassWorkerImplementation {
|
|||||||
private async processImporters(
|
private async processImporters(
|
||||||
importers: Iterable<Importers>,
|
importers: Iterable<Importers>,
|
||||||
url: string,
|
url: string,
|
||||||
options: FileImporterWithRequestContextOptions,
|
options: CanonicalizeContext,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
for (const importer of importers) {
|
for (const importer of importers) {
|
||||||
if (this.isImporter(importer)) {
|
if (this.isImporter(importer)) {
|
||||||
|
@ -93,7 +93,14 @@ parentPort.on('message', (message: RenderRequestMessage) => {
|
|||||||
const proxyImporter: FileImporter<'sync'> = {
|
const proxyImporter: FileImporter<'sync'> = {
|
||||||
findFileUrl: (url, options) => {
|
findFileUrl: (url, options) => {
|
||||||
Atomics.store(importerSignal, 0, 0);
|
Atomics.store(importerSignal, 0, 0);
|
||||||
workerImporterPort.postMessage({ id, url, options });
|
workerImporterPort.postMessage({
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
containingUrl: options.containingUrl ? fileURLToPath(options.containingUrl) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
Atomics.wait(importerSignal, 0, 0);
|
Atomics.wait(importerSignal, 0, 0);
|
||||||
|
|
||||||
const result = receiveMessageOnPort(workerImporterPort)?.message as string | null;
|
const result = receiveMessageOnPort(workerImporterPort)?.message as string | null;
|
||||||
|
@ -8,16 +8,13 @@
|
|||||||
|
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import type { FileImporter } from 'sass';
|
import type { FileImporter } from 'sass';
|
||||||
import type { Configuration, LoaderContext, RuleSetUseItem } from 'webpack';
|
import type { Configuration, LoaderContext, RuleSetUseItem } from 'webpack';
|
||||||
import { WebpackConfigOptions } from '../../../utils/build-options';
|
import { WebpackConfigOptions } from '../../../utils/build-options';
|
||||||
import { useLegacySass } from '../../../utils/environment-options';
|
import { useLegacySass } from '../../../utils/environment-options';
|
||||||
import { findTailwindConfigurationFile } from '../../../utils/tailwind';
|
import { findTailwindConfigurationFile } from '../../../utils/tailwind';
|
||||||
import {
|
import { SassWorkerImplementation } from '../../sass/sass-service';
|
||||||
FileImporterWithRequestContextOptions,
|
|
||||||
SassWorkerImplementation,
|
|
||||||
} from '../../sass/sass-service';
|
|
||||||
import { SassLegacyWorkerImplementation } from '../../sass/sass-service-legacy';
|
import { SassLegacyWorkerImplementation } from '../../sass/sass-service-legacy';
|
||||||
import {
|
import {
|
||||||
AnyComponentStyleBudgetChecker,
|
AnyComponentStyleBudgetChecker,
|
||||||
@ -405,28 +402,20 @@ function getSassResolutionImporter(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
findFileUrl: async (
|
findFileUrl: async (url, { fromImport, containingUrl }): Promise<URL | null> => {
|
||||||
url,
|
|
||||||
{ fromImport, previousResolvedModules }: FileImporterWithRequestContextOptions,
|
|
||||||
): Promise<URL | null> => {
|
|
||||||
if (url.charAt(0) === '.') {
|
if (url.charAt(0) === '.') {
|
||||||
// Let Sass handle relative imports.
|
// Let Sass handle relative imports.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resolveDir = root;
|
||||||
|
if (containingUrl) {
|
||||||
|
resolveDir = path.dirname(fileURLToPath(containingUrl));
|
||||||
|
}
|
||||||
|
|
||||||
const resolve = fromImport ? resolveImport : resolveModule;
|
const resolve = fromImport ? resolveImport : resolveModule;
|
||||||
// Try to resolve from root of workspace
|
// Try to resolve from root of workspace
|
||||||
let result = await tryResolve(resolve, root, url);
|
const result = await tryResolve(resolve, resolveDir, url);
|
||||||
|
|
||||||
// Try to resolve from previously resolved modules.
|
|
||||||
if (!result && previousResolvedModules) {
|
|
||||||
for (const path of previousResolvedModules) {
|
|
||||||
result = await tryResolve(resolve, path, url);
|
|
||||||
if (result) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result ? pathToFileURL(result) : null;
|
return result ? pathToFileURL(result) : null;
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user