Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/angular/build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ ts_project(
":node_modules/rolldown",
":node_modules/rollup",
":node_modules/sass",
":node_modules/sass-embedded",
":node_modules/source-map-support",
":node_modules/tinyglobby",
":node_modules/vite",
Expand Down
1 change: 1 addition & 0 deletions packages/angular/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"piscina": "5.3.1",
"rolldown": "1.2.5",
"sass": "1.103.1",
"sass-embedded": "1.103.1",
"semver": "7.8.5",
"source-map-support": "0.5.21",
"tinyglobby": "0.2.17",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export {
export type { ExternalResultMetadata } from './tools/esbuild/bundler-execution-result';
export { emitFilesToDisk } from './tools/esbuild/utils';
export { transformSupportedBrowsersToTargets } from './tools/esbuild/target';
export { SassWorkerImplementation } from './tools/sass/sass-service';
export { SassWorkerImplementation } from './tools/sass/sass-worker-implementation';

export { SourceFileCache } from './tools/esbuild/angular/source-file-cache';
export { Cache } from './tools/esbuild/cache';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,27 @@
import type { OnLoadResult, PartialMessage, PartialNote, ResolveResult } from 'esbuild';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { CanonicalizeContext, CompileResult, Exception, Syntax } from 'sass';
import type { SassWorkerImplementation } from '../../sass/sass-service';
import type { CanonicalizeContext, CompileResult, Exception, Syntax } from 'sass-embedded';
import { useSassWorker } from '../../../utils/environment-options';
import type { SassServiceImplementation } from '../../sass/sass-service';
import { MemoryCache } from '../cache';
import { StylesheetLanguage, StylesheetPluginOptions } from './stylesheet-plugin-factory';

let sassWorkerPool: SassWorkerImplementation | undefined;
let sassWorkerPoolPromise: Promise<SassWorkerImplementation> | undefined;
let sassService: SassServiceImplementation | undefined;
let sassServicePromise: Promise<SassServiceImplementation> | undefined;

function isSassException(error: unknown): error is Exception {
return !!error && typeof error === 'object' && 'sassMessage' in error;
}

export function shutdownSassWorkerPool(): void {
if (sassWorkerPool) {
void sassWorkerPool.close();
sassWorkerPool = undefined;
} else if (sassWorkerPoolPromise) {
void sassWorkerPoolPromise.then(shutdownSassWorkerPool);
if (sassService) {
void sassService.close();
sassService = undefined;
} else if (sassServicePromise) {
void sassServicePromise.then(shutdownSassWorkerPool);
}
sassWorkerPoolPromise = undefined;
sassServicePromise = undefined;
}

export const SassStylesheetLanguage = Object.freeze<StylesheetLanguage>({
Expand Down Expand Up @@ -78,13 +79,21 @@ async function compileString(
resolveUrl: (url: string, options: CanonicalizeContext) => Promise<ResolveResult>,
): Promise<OnLoadResult> {
// Lazily load Sass when a Sass file is found
if (sassWorkerPool === undefined) {
if (sassWorkerPoolPromise === undefined) {
sassWorkerPoolPromise = import('../../sass/sass-service').then(
(sassService) => new sassService.SassWorkerImplementation(true),
);
if (sassService === undefined) {
if (sassServicePromise === undefined) {
sassServicePromise = useSassWorker
? import('../../sass/sass-worker-implementation').then(
(sassService) => new sassService.SassWorkerImplementation(true),
)
: import('../../sass/sass-async-compiler-implementation').then(
(sassService) => new sassService.SassAsyncCompilerImplementation(),
);
}
try {
sassService = await sassServicePromise;
} finally {
sassServicePromise = undefined;
}
sassWorkerPool = await sassWorkerPoolPromise;
}

// Cache is currently local to individual compile requests.
Expand All @@ -99,7 +108,7 @@ async function compileString(
const { silenceDeprecations, futureDeprecations, fatalDeprecations } = options.sass ?? {};

try {
const { css, sourceMap, loadedUrls } = await sassWorkerPool.compileStringAsync(data, {
const { css, sourceMap, loadedUrls } = await sassService.compileStringAsync(data, {
url: pathToFileURL(filePath),
style: 'expanded',
syntax,
Expand Down Expand Up @@ -226,7 +235,7 @@ function* extractFilesFromStack(stack: string): Iterable<string> {
}

if (path) {
// Stack paths from dart-sass are relative to the current working directory (not input file or workspace root)
// Stack paths from sass are relative to the current working directory (not input file or workspace root)
yield join(cwd, path);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, extname } from 'node:path';
import type { Options } from 'sass';
import type { Options } from 'sass-embedded';
import { glob } from 'tinyglobby';
import { assertIsError } from '../../../utils/error';
import type { PostcssConfiguration } from '../../../utils/postcss-configuration';
Expand Down
40 changes: 39 additions & 1 deletion packages/angular/build/src/tools/sass/rebasing-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MagicString } from 'magic-string';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { basename, dirname, extname, join, relative } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass';
import type { CanonicalizeContext, Importer, ImporterResult, Syntax } from 'sass-embedded';
import { assertIsError } from '../../utils/error';
import { toPosixPath } from '../../utils/path';
import { findUrls } from './lexer';
Expand Down Expand Up @@ -343,6 +343,44 @@ export class ModuleUrlRebasingImporter extends RelativeUrlRebasingImporter {
}
}

/**
* Provides the Sass importer logic to resolve module (npm package) stylesheet imports asynchronously
* and also rebase any `url()` function usage within those stylesheets.
*/
export class AsyncModuleUrlRebasingImporter implements Importer<'async'> {
private relativeImporter: RelativeUrlRebasingImporter;

constructor(
entryDirectory: string,
directoryCache: Map<string, DirectoryEntry>,
rebaseSourceMaps: Map<string, DecodedSourceMap> | undefined,
private finder: (
specifier: string,
options: CanonicalizeContext,
) => Promise<URL | null> | URL | null,
) {
this.relativeImporter = new RelativeUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
);
}

async canonicalize(url: string, options: CanonicalizeContext): Promise<URL | null> {
if (url.startsWith('file://')) {
return this.relativeImporter.canonicalize(url, options);
}

const result = await this.finder(url, options);

return result ? this.relativeImporter.canonicalize(result.href, options) : null;
}

load(canonicalUrl: URL): ImporterResult | null {
return this.relativeImporter.load(canonicalUrl);
}
}

/**
* Provides the Sass importer logic to resolve load paths located stylesheet imports via both import and
* use rules and also rebase any `url()` function usage within those stylesheets. The rebasing will ensure that
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import mergeSourceMaps, { type DecodedSourceMap, type RawSourceMap } from '@ampproject/remapping';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import type {
AsyncCompiler,
CanonicalizeContext,
CompileResult,
FileImporter,
Importer,
NodePackageImporter,
StringOptions,
} from 'sass-embedded';
import {
AsyncModuleUrlRebasingImporter,
DirectoryEntry,
LoadPathsUrlRebasingImporter,
RelativeUrlRebasingImporter,
} from './rebasing-importer';
import { type SassServiceImplementation, isFileImporter } from './sass-service';

/**
* A Sass renderer implementation that uses the persistent Dart Sass embedded compiler
* daemon (`sass-embedded`) communicating over standard input/output with protocol buffers.
*/
export class SassAsyncCompilerImplementation implements SassServiceImplementation {
#asyncCompiler: AsyncCompiler | undefined;
#asyncCompilerPromise: Promise<AsyncCompiler> | undefined;

async #ensureAsyncCompiler(): Promise<AsyncCompiler> {
if (this.#asyncCompiler) {
return this.#asyncCompiler;
}

// Import and initialize the async compiler on the main thread.
this.#asyncCompilerPromise ??= import('sass-embedded').then(({ initAsyncCompiler }) =>
initAsyncCompiler(),
);

try {
this.#asyncCompiler = await this.#asyncCompilerPromise;
} finally {
this.#asyncCompilerPromise = undefined;
}

return this.#asyncCompiler;
}

/**
* Provides information about the Sass implementation.
* This mimics enough of the `sass-embedded` value to be used with the `sass-loader`.
*/
get info(): string {
return 'sass-embedded\tasync-compiler';
}

/**
* The synchronous render function is not used by the `sass-loader`.
*/
compileString(): never {
throw new Error('Sass compileString is not supported.');
}

/**
* Asynchronously request a Sass stylesheet to be rendered using the native embedded compiler.
*
* @param source The contents to compile.
* @param options The `sass-embedded` options to use when rendering the stylesheet.
*/
async compileStringAsync(
source: string,
options: StringOptions<'async'>,
): Promise<CompileResult> {
// The CLI's configuration does not use or expose the ability to define custom Sass functions
if (options.functions && Object.keys(options.functions).length > 0) {
throw new Error('Sass custom functions are not supported.');
}

const { functions, importers, importer, url, logger, ...serializableOptions } = options;

let finalImporters:
(Importer<'async'> | FileImporter<'async'> | NodePackageImporter)[] | undefined;
let loadPaths = options.loadPaths;
const entryDirectory = url ? dirname(fileURLToPath(url)) : process.cwd();
const directoryCache = new Map<string, DirectoryEntry>();
const rebaseSourceMaps = options.sourceMap ? new Map<string, DecodedSourceMap>() : undefined;

if (importers?.length) {
for (const importer of importers) {
if (!isFileImporter(importer)) {
throw new Error('Only File Importers are supported.');
}
}

finalImporters = [
new AsyncModuleUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
async (specifier: string, options: CanonicalizeContext): Promise<URL | null> => {
for (const importer of importers) {
const result = await (importer as FileImporter<'async'>).findFileUrl(
specifier,
options,
);
if (result) {
return result;
}
}

return null;
},
),
];
}
Comment thread
alan-agius4 marked this conversation as resolved.

if (loadPaths?.length) {
finalImporters ??= [];
finalImporters.push(
new LoadPathsUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
loadPaths,
),
);
loadPaths = undefined;
}

const relativeImporter = new RelativeUrlRebasingImporter(
entryDirectory,
directoryCache,
rebaseSourceMaps,
);

const compiler = await this.#ensureAsyncCompiler();
const result = await compiler.compileStringAsync(source, {
...serializableOptions,
url,
loadPaths,
importers: finalImporters,
importer: relativeImporter,
logger,
});

if (result.sourceMap && rebaseSourceMaps?.size) {
result.sourceMap = mergeSourceMaps(
result.sourceMap as unknown as RawSourceMap,
(file, context) => (file !== context.importer ? rebaseSourceMaps.get(file) : null),
) as unknown as typeof result.sourceMap;
}

return result;
}

/**
* Shutdown the native embedded Sass compiler daemon.
* @returns A void promise that resolves when closing is complete.
*/
async close(): Promise<void> {
if (this.#asyncCompilerPromise) {
try {
await this.#ensureAsyncCompiler();
} catch {
// Ignore compiler initialization failures on shutdown
}
}
Comment thread
alan-agius4 marked this conversation as resolved.

if (this.#asyncCompiler) {
const compiler = this.#asyncCompiler;
this.#asyncCompiler = undefined;
await compiler.dispose();
}
}
}
Loading