From 8686fc5e78096f04b06f2c6ea259bca4c8c84566 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:18:03 +0000 Subject: [PATCH] perf(@angular/build): use native sass-embedded compiler daemon Replaces the Piscina-based worker thread pool executing pure-JS Dart Sass (`sass` compiled via `dart2js`) with a native Dart Sass asynchronous compiler daemon (`sass-embedded`). The build system previously relied on multi-threaded worker pools with synchronous compilation to bypass performance overheads inherent to pure-JS asynchronous Dart Sass. However, this approach incurred significant V8 heap and isolate duplication across worker threads, as well as thread contention and blocking due to `Atomics.wait()` synchronization loops during custom URL and module rebasing. With `sass-embedded`, Sass compilation is delegated to a long-lived native Dart AOT binary communicating asynchronously via Protocol Buffers over stdio pipes. This eliminates duplicate V8 isolates, replaces synchronous `Atomics.wait()` with event-driven asynchronous message handling, and dramatically reduces memory pressure on the Node.js process. The compiler infrastructure is structured around a shared `SassServiceImplementation` interface with two isolated implementations dynamically loaded on demand: - `SassAsyncCompilerImplementation`: Manages the native `sass-embedded` daemon lifecycle and asynchronous URL rebasing importers (`AsyncModuleUrlRebasingImporter`). - `SassWorkerImplementation`: Retained as an isolated worker pool fallback. Environment Variable & Fallbacks: - `NG_BUILD_SASS_WORKER=1` (or `true`): Forces the build system to fall back to the pure-JS `SassWorkerImplementation` using worker threads. - In restricted environments where native platform binaries cannot be spawned (such as WebContainers detected via `process.versions.webcontainer`), the build system automatically falls back to `SassWorkerImplementation` without requiring manual configuration. ### Benchmarks: Sass Service in Isolation #### 10 Small Files (Angular Components) | Metric | Worker Pool (`main`) | Native Async (`this change`) | Delta | Speedup / Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Start | 369.04 ms | 229.89 ms | -139.15 ms | 1.61x faster (-37.7%) | | Warm Median | 22.46 ms | 5.27 ms | -17.19 ms | 4.26x faster (-76.5%) | | Warm Mean | 25.82 ms | 7.25 ms | -18.57 ms | 3.56x faster (-71.9%) | | Warm P95 | 37.30 ms | 14.55 ms | -22.75 ms | 2.56x faster (-61.0%) | | Warm Throughput | 445.2 files/s | 1,899.3 files/s | +1,454.1 files/s | 4.27x throughput (+326.6%) | | Incremental Median | 6.54 ms | 2.08 ms | -4.46 ms | 3.14x faster (-68.2%) | | Incremental Throughput | 153.0 files/s | 480.5 files/s | +327.5 files/s | 3.14x throughput (+214.0%) | | Peak Memory RSS | 281.16 MB | 78.73 MB | -202.43 MB | -72.0% memory saved | #### 10 Large Files (Design System Tokens & Themes) | Metric | Worker Pool (`main`) | Native Async (`this change`) | Delta | Speedup / Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Start | 823.95 ms | 386.25 ms | -437.70 ms | 2.13x faster (-53.1%) | | Warm Median | 231.85 ms | 175.64 ms | -56.21 ms | 1.32x faster (-24.2%) | | Warm Mean | 241.50 ms | 172.22 ms | -69.28 ms | 1.40x faster (-28.7%) | | Warm P95 | 293.09 ms | 215.48 ms | -77.61 ms | 1.36x faster (-26.5%) | | Warm Throughput | 43.1 files/s | 56.9 files/s | +13.8 files/s | 1.32x throughput (+32.0%) | | Incremental Median | 72.63 ms | 35.34 ms | -37.29 ms | 2.06x faster (-51.3%) | | Incremental Throughput | 13.8 files/s | 28.3 files/s | +14.5 files/s | 2.05x throughput (+105.4%) | | Peak Memory RSS | 986.47 MB | 112.34 MB | -874.13 MB | -88.6% memory saved | ### Benchmarks: Real Angular CLI Application Scenarios (ng build) #### Small App (5 small files) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 3,201.3 ms | 2,950.2 ms | -251.1 ms | -7.8% | | Cold CLI Time | 2,456.0 ms | 2,302.0 ms | -154.0 ms | -6.3% | | Cold Peak RSS | 1,227.3 MB | 1,058.9 MB | -168.4 MB | -13.7% | | Warm (Avg) Wall Time | 2,762.0 ms | 2,475.3 ms | -286.7 ms | -10.4% | | Warm (Avg) CLI Time | 2,071.3 ms | 1,820.3 ms | -251.0 ms | -12.1% | | Warm (Avg) Peak RSS | 1,102.3 MB | 894.8 MB | -207.5 MB | -18.8% | #### Small App (5 large files) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 3,605.9 ms | 3,175.8 ms | -430.1 ms | -11.9% | | Cold CLI Time | 2,874.0 ms | 2,482.0 ms | -392.0 ms | -13.6% | | Cold Peak RSS | 1,420.2 MB | 1,072.3 MB | -347.9 MB | -24.5% | | Warm (Avg) Wall Time | 3,123.7 ms | 2,682.5 ms | -441.2 ms | -14.1% | | Warm (Avg) CLI Time | 2,435.7 ms | 2,028.7 ms | -407.0 ms | -16.7% | | Warm (Avg) Peak RSS | 1,278.2 MB | 911.3 MB | -366.9 MB | -28.7% | #### Large App (500 files: 250 large + 250 small) | Phase / Metric | Main (`NG_BUILD_SASS_WORKER=1`) | This Change (Native Async) | Delta | Improvement | | :--- | :---: | :---: | :---: | :---: | | Cold Wall Time | 15,259.7 ms | 15,348.6 ms | +88.9 ms | +0.6% (Parity) | | Cold CLI Time | 14,425.0 ms | 14,373.0 ms | -52.0 ms | -0.4% (Parity) | | Cold Peak RSS | 2,305.1 MB | 1,582.7 MB | -722.4 MB | -31.3% memory saved | | Warm Run 1 Wall Time | 14,673.1 ms | 14,277.1 ms | -396.0 ms | -2.7% | | Warm Run 1 CLI Time | 13,812.0 ms | 13,375.0 ms | -437.0 ms | -3.2% | | Warm Run 1 Peak RSS | 2,180.5 MB | 1,485.6 MB | -694.9 MB | -31.9% memory saved | | Warm Run 2 Wall Time | 14,771.2 ms | 14,502.7 ms | -268.5 ms | -1.8% | | Warm Run 2 CLI Time | 13,943.0 ms | 13,527.0 ms | -416.0 ms | -3.0% | | Warm Run 2 Peak RSS | 2,220.8 MB | 1,493.3 MB | -727.5 MB | -32.8% memory saved | | Warm (Avg) Peak RSS | 2,200.7 MB | 1,489.5 MB | -711.2 MB | -32.3% memory saved | Closes #29319 --- packages/angular/build/BUILD.bazel | 1 + packages/angular/build/package.json | 1 + packages/angular/build/src/private.ts | 2 +- .../esbuild/stylesheets/sass-language.ts | 45 +-- .../stylesheets/stylesheet-plugin-factory.ts | 2 +- .../build/src/tools/sass/rebasing-importer.ts | 40 ++- .../sass-async-compiler-implementation.ts | 182 ++++++++++++ .../build/src/tools/sass/sass-service.ts | 248 +--------------- .../tools/sass/sass-worker-implementation.ts | 245 ++++++++++++++++ .../angular/build/src/tools/sass/worker.ts | 5 +- .../build/src/utils/environment-options.ts | 7 + pnpm-lock.yaml | 268 +++++++++++++++++- 12 files changed, 771 insertions(+), 275 deletions(-) create mode 100644 packages/angular/build/src/tools/sass/sass-async-compiler-implementation.ts create mode 100644 packages/angular/build/src/tools/sass/sass-worker-implementation.ts diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 75eedd6a7f5f..e63b084ec458 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -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", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 0a998207cf27..609cf3132ce0 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -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", diff --git a/packages/angular/build/src/private.ts b/packages/angular/build/src/private.ts index 1e99d84c9832..315636decae2 100644 --- a/packages/angular/build/src/private.ts +++ b/packages/angular/build/src/private.ts @@ -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'; diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index 4bccfb3adba4..92c75d76d7ec 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -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 | undefined; +let sassService: SassServiceImplementation | undefined; +let sassServicePromise: Promise | 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({ @@ -78,13 +79,21 @@ async function compileString( resolveUrl: (url: string, options: CanonicalizeContext) => Promise, ): Promise { // 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. @@ -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, @@ -226,7 +235,7 @@ function* extractFilesFromStack(stack: string): Iterable { } 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); } } diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index 2c40e007350e..79337fe1652e 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -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'; diff --git a/packages/angular/build/src/tools/sass/rebasing-importer.ts b/packages/angular/build/src/tools/sass/rebasing-importer.ts index 5d2a4ddc2267..e9509574f455 100644 --- a/packages/angular/build/src/tools/sass/rebasing-importer.ts +++ b/packages/angular/build/src/tools/sass/rebasing-importer.ts @@ -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'; @@ -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, + rebaseSourceMaps: Map | undefined, + private finder: ( + specifier: string, + options: CanonicalizeContext, + ) => Promise | URL | null, + ) { + this.relativeImporter = new RelativeUrlRebasingImporter( + entryDirectory, + directoryCache, + rebaseSourceMaps, + ); + } + + async canonicalize(url: string, options: CanonicalizeContext): Promise { + 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 diff --git a/packages/angular/build/src/tools/sass/sass-async-compiler-implementation.ts b/packages/angular/build/src/tools/sass/sass-async-compiler-implementation.ts new file mode 100644 index 000000000000..cdf483994f12 --- /dev/null +++ b/packages/angular/build/src/tools/sass/sass-async-compiler-implementation.ts @@ -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 | undefined; + + async #ensureAsyncCompiler(): Promise { + 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 { + // 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(); + const rebaseSourceMaps = options.sourceMap ? new Map() : 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 => { + for (const importer of importers) { + const result = await (importer as FileImporter<'async'>).findFileUrl( + specifier, + options, + ); + if (result) { + return result; + } + } + + return null; + }, + ), + ]; + } + + 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 { + if (this.#asyncCompilerPromise) { + try { + await this.#ensureAsyncCompiler(); + } catch { + // Ignore compiler initialization failures on shutdown + } + } + + if (this.#asyncCompiler) { + const compiler = this.#asyncCompiler; + this.#asyncCompiler = undefined; + await compiler.dispose(); + } + } +} diff --git a/packages/angular/build/src/tools/sass/sass-service.ts b/packages/angular/build/src/tools/sass/sass-service.ts index 2df6f85f7a52..1a7c3b7866d8 100644 --- a/packages/angular/build/src/tools/sass/sass-service.ts +++ b/packages/angular/build/src/tools/sass/sass-service.ts @@ -6,261 +6,33 @@ * found in the LICENSE file at https://angular.dev/license */ -import assert from 'node:assert'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { MessageChannel } from 'node:worker_threads'; import type { - CanonicalizeContext, CompileResult, - Deprecation, - Exception, FileImporter, Importer, NodePackageImporter, - SourceSpan, StringOptions, -} from 'sass'; -import { maxWorkers } from '../../utils/environment-options'; -import { WorkerPool } from '../../utils/worker-pool'; - -// Polyfill Symbol.dispose if not present -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(Symbol as any).dispose ??= Symbol('Symbol Dispose'); +} from 'sass-embedded'; /** - * The maximum number of Workers that will be created to execute render requests. + * Common interface for Sass service implementations. */ -const MAX_RENDER_WORKERS = maxWorkers; +export interface SassServiceImplementation { + readonly info: string; + compileStringAsync(source: string, options: StringOptions<'async'>): Promise; + close(): Promise; +} /** * All available importer types. */ -type Importers = +export type Importers = | Importer<'sync'> | Importer<'async'> | FileImporter<'sync'> | FileImporter<'async'> | NodePackageImporter; -export interface SerializableVersion { - major: number; - minor: number; - patch: number; -} - -export interface SerializableDeprecation extends Omit { - /** The version this deprecation first became active in. */ - deprecatedIn: SerializableVersion | null; - - /** The version this deprecation became obsolete in. */ - obsoleteIn: SerializableVersion | null; -} - -export type SerializableWarningMessage = ( - | { - deprecation: true; - deprecationType: SerializableDeprecation; - } - | { deprecation: false } -) & { - message: string; - span?: Omit & { url?: string }; - stack?: string; -}; - -/** - * A response from the Sass render Worker containing the result of the operation. - */ -interface RenderResponseMessage { - error?: Exception; - result?: Omit & { loadedUrls: string[] }; - warnings?: SerializableWarningMessage[]; -} - -/** - * A Sass renderer implementation that provides an interface that can be used by Webpack's - * `sass-loader`. The implementation uses a Worker thread to perform the Sass rendering - * with the `dart-sass` package. The `dart-sass` synchronous render function is used within - * the worker which can be up to two times faster than the asynchronous variant. - */ -export class SassWorkerImplementation { - #workerPool: WorkerPool | undefined; - - constructor( - private readonly rebase = false, - readonly maxThreads = MAX_RENDER_WORKERS, - ) {} - - #ensureWorkerPool(): WorkerPool { - this.#workerPool ??= new WorkerPool({ - filename: require.resolve('./worker'), - maxThreads: this.maxThreads, - }); - - return this.#workerPool; - } - - /** - * Provides information about the Sass implementation. - * This mimics enough of the `dart-sass` value to be used with the `sass-loader`. - */ - get info(): string { - return 'dart-sass\tworker'; - } - - /** - * 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 renderered. - * - * @param source The contents to compile. - * @param options The `dart-sass` options to use when rendering the stylesheet. - */ - async compileStringAsync( - source: string, - options: StringOptions<'async'>, - ): Promise { - // The `functions`, `logger` and `importer` options are JavaScript functions that cannot be transferred. - // If any additional function options are added in the future, they must be excluded as well. - const { functions, importers, url, logger, ...serializableOptions } = options; - - // The CLI's configuration does not use or expose the ability to define custom Sass functions - if (functions && Object.keys(functions).length > 0) { - throw new Error('Sass custom functions are not supported.'); - } - - using importerChannel = importers?.length ? this.#createImporterChannel(importers) : undefined; - - const response = (await this.#ensureWorkerPool().run( - { - source, - importerChannel, - hasLogger: !!logger, - rebase: this.rebase, - options: { - ...serializableOptions, - // URL is not serializable so to convert to string here and back to URL in the worker. - url: url ? fileURLToPath(url) : undefined, - }, - }, - { - transferList: importerChannel ? [importerChannel.port] : undefined, - }, - )) as RenderResponseMessage; - - const { result, error, warnings } = response; - - if (warnings && logger?.warn) { - for (const { message, span, ...options } of warnings) { - logger.warn(message, { - ...options, - span: span && { - ...span, - url: span.url ? pathToFileURL(span.url) : undefined, - }, - }); - } - } - - if (error) { - // Convert stringified url value required for cloning back to a URL object - const url = error.span?.url as string | undefined; - if (url) { - error.span.url = pathToFileURL(url); - } - - throw error; - } - - assert(result, 'Sass render worker should always return a result or an error'); - - return { - ...result, - // URL is not serializable so in the worker we convert to string and here back to URL. - loadedUrls: result.loadedUrls.map((p) => pathToFileURL(p)), - }; - } - - /** - * Shutdown the Sass render worker. - * Executing this method will stop any pending render requests. - * @returns A void promise that resolves when closing is complete. - */ - async close(): Promise { - if (this.#workerPool) { - try { - await this.#workerPool.destroy(); - } finally { - this.#workerPool = undefined; - } - } - } - - #createImporterChannel(importers: Iterable) { - const { port1: mainImporterPort, port2: workerImporterPort } = new MessageChannel(); - const importerSignal = new Int32Array(new SharedArrayBuffer(4)); - - mainImporterPort.on( - 'message', - ({ url, options }: { url: string; options: CanonicalizeContext }) => { - this.processImporters(importers, url, { - ...options, - // 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) => { - mainImporterPort.postMessage(result); - }) - .catch((error) => { - mainImporterPort.postMessage(error); - }) - .finally(() => { - Atomics.store(importerSignal, 0, 1); - Atomics.notify(importerSignal, 0); - }); - }, - ); - - mainImporterPort.unref(); - - return { - port: workerImporterPort, - signal: importerSignal, - [Symbol.dispose]() { - mainImporterPort.close(); - }, - }; - } - - private async processImporters( - importers: Iterable, - url: string, - options: CanonicalizeContext, - ): Promise { - for (const importer of importers) { - if (!this.isFileImporter(importer)) { - // Importer - throw new Error('Only File Importers are supported.'); - } - - // File importer (Can be sync or aync). - const result = await importer.findFileUrl(url, options); - if (result) { - return fileURLToPath(result); - } - } - - return null; - } - - private isFileImporter(value: Importers): value is FileImporter { - return 'findFileUrl' in value; - } +export function isFileImporter(value: Importers): value is FileImporter { + return 'findFileUrl' in value; } diff --git a/packages/angular/build/src/tools/sass/sass-worker-implementation.ts b/packages/angular/build/src/tools/sass/sass-worker-implementation.ts new file mode 100644 index 000000000000..9208bef3a803 --- /dev/null +++ b/packages/angular/build/src/tools/sass/sass-worker-implementation.ts @@ -0,0 +1,245 @@ +/** + * @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 assert from 'node:assert'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { MessageChannel } from 'node:worker_threads'; +import type { + CanonicalizeContext, + CompileResult, + Deprecation, + Exception, + SourceSpan, + StringOptions, +} from 'sass-embedded'; +import { maxWorkers } from '../../utils/environment-options'; +import { WorkerPool } from '../../utils/worker-pool'; +import { type Importers, type SassServiceImplementation, isFileImporter } from './sass-service'; + +// Polyfill Symbol.dispose if not present +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(Symbol as any).dispose ??= Symbol('Symbol Dispose'); + +/** + * The maximum number of Workers that will be created to execute render requests. + */ +const MAX_RENDER_WORKERS = maxWorkers; + +export interface SerializableVersion { + major: number; + minor: number; + patch: number; +} + +export interface SerializableDeprecation extends Omit { + /** The version this deprecation first became active in. */ + deprecatedIn: SerializableVersion | null; + + /** The version this deprecation became obsolete in. */ + obsoleteIn: SerializableVersion | null; +} + +export type SerializableWarningMessage = ( + | { + deprecation: true; + deprecationType: SerializableDeprecation; + } + | { deprecation: false } +) & { + message: string; + span?: Omit & { url?: string }; + stack?: string; +}; + +/** + * A response from the Sass render Worker containing the result of the operation. + */ +export interface RenderResponseMessage { + error?: Exception; + result?: Omit & { loadedUrls: string[] }; + warnings?: SerializableWarningMessage[]; +} + +/** + * A Sass renderer implementation that provides an interface that can be used by Webpack's + * `sass-loader` or as a fallback in environments that do not support native binaries. + * The implementation uses a Worker thread pool to perform Sass rendering with the pure-JS + * `sass` package. + */ +export class SassWorkerImplementation implements SassServiceImplementation { + #workerPool: WorkerPool | undefined; + + constructor( + private readonly rebase = false, + readonly maxThreads = MAX_RENDER_WORKERS, + ) {} + + #ensureWorkerPool(): WorkerPool { + this.#workerPool ??= new WorkerPool({ + filename: require.resolve('./worker'), + maxThreads: this.maxThreads, + }); + + return this.#workerPool; + } + + /** + * Provides information about the Sass implementation. + * This mimics enough of the `sass` value to be used with the `sass-loader`. + */ + get info(): string { + return 'dart-sass\tworker'; + } + + /** + * 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 worker threads. + * + * @param source The contents to compile. + * @param options The Sass options to use when rendering the stylesheet. + */ + async compileStringAsync( + source: string, + options: StringOptions<'async'>, + ): Promise { + // 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; + using importerChannel = importers?.length ? this.#createImporterChannel(importers) : undefined; + + const response = (await this.#ensureWorkerPool().run( + { + source, + importerChannel, + hasLogger: !!logger, + rebase: this.rebase, + options: { + ...serializableOptions, + // URL is not serializable so to convert to string here and back to URL in the worker. + url: url ? fileURLToPath(url) : undefined, + }, + }, + { + transferList: importerChannel ? [importerChannel.port] : undefined, + }, + )) as RenderResponseMessage; + + const { result, error, warnings } = response; + + if (warnings && logger?.warn) { + for (const { message, span, ...options } of warnings) { + logger.warn(message, { + ...options, + span: span && { + ...span, + url: span.url ? pathToFileURL(span.url) : undefined, + }, + }); + } + } + + if (error) { + // Convert stringified url value required for cloning back to a URL object + const url = error.span?.url as string | undefined; + if (url) { + error.span.url = pathToFileURL(url); + } + + throw error; + } + + assert(result, 'Sass render worker should always return a result or an error'); + + return { + ...result, + // URL is not serializable so in the worker we convert to string and here back to URL. + loadedUrls: result.loadedUrls.map((p) => pathToFileURL(p)), + }; + } + + /** + * Shutdown the Sass render worker. + * Executing this method will stop any pending render requests. + * @returns A void promise that resolves when closing is complete. + */ + async close(): Promise { + if (this.#workerPool) { + const pool = this.#workerPool; + this.#workerPool = undefined; + await pool.destroy(); + } + } + + #createImporterChannel(importers: Iterable) { + const { port1: mainImporterPort, port2: workerImporterPort } = new MessageChannel(); + const importerSignal = new Int32Array(new SharedArrayBuffer(4)); + + mainImporterPort.on( + 'message', + ({ url, options }: { url: string; options: CanonicalizeContext }) => { + this.processImporters(importers, url, { + ...options, + // 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) => { + mainImporterPort.postMessage(result); + }) + .catch((error) => { + mainImporterPort.postMessage(error); + }) + .finally(() => { + Atomics.store(importerSignal, 0, 1); + Atomics.notify(importerSignal, 0); + }); + }, + ); + + mainImporterPort.unref(); + + return { + port: workerImporterPort, + signal: importerSignal, + [Symbol.dispose]() { + mainImporterPort.close(); + }, + }; + } + + private async processImporters( + importers: Iterable, + url: string, + options: CanonicalizeContext, + ): Promise { + for (const importer of importers) { + if (!isFileImporter(importer)) { + // Importer + throw new Error('Only File Importers are supported.'); + } + + // File importer (Can be sync or aync). + const result = await importer.findFileUrl(url, options); + if (result) { + return fileURLToPath(result); + } + } + + return null; + } +} diff --git a/packages/angular/build/src/tools/sass/worker.ts b/packages/angular/build/src/tools/sass/worker.ts index e4167a3d1c69..2416fe21e5cd 100644 --- a/packages/angular/build/src/tools/sass/worker.ts +++ b/packages/angular/build/src/tools/sass/worker.ts @@ -25,7 +25,10 @@ import { RelativeUrlRebasingImporter, sassBindWorkaround, } from './rebasing-importer'; -import type { SerializableDeprecation, SerializableWarningMessage } from './sass-service'; +import type { + SerializableDeprecation, + SerializableWarningMessage, +} from './sass-worker-implementation'; /** * A request to render a Sass stylesheet using the supplied options. diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index 206467e929a0..c9ac132e7887 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -201,6 +201,13 @@ export const usePartialSsrBuild = parseTristate(process.env['NG_BUILD_PARTIAL_SS */ export const useBabelLinker = parseTristate(process.env['NG_BUILD_BABEL_LINKER']) === true; +/** + * When `NG_BUILD_SASS_WORKER` is enabled (`1` or `true`), the worker-based + * Sass implementation will be used instead of the native asynchronous compiler. + */ +export const useSassWorker = + !!process.versions.webcontainer || parseTristate(process.env['NG_BUILD_SASS_WORKER']) === true; + const bazelBinDirectory = process.env['BAZEL_BINDIR']; const bazelExecRoot = process.env['JS_BINARY__EXECROOT']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad8a28e1ca05..6bab452d7092 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,7 +321,7 @@ importers: version: 7.8.2 vitest: specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) packages/angular/build: dependencies: @@ -342,7 +342,7 @@ importers: version: 2.6.0 '@vitejs/plugin-basic-ssl': specifier: 2.3.0 - version: 2.3.0(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 2.3.0(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) beasties: specifier: 0.4.3 version: 0.4.3 @@ -388,6 +388,9 @@ importers: sass: specifier: 1.103.1 version: 1.103.1 + sass-embedded: + specifier: 1.103.1 + version: 1.103.1 semver: specifier: 7.8.5 version: 7.8.5 @@ -399,7 +402,7 @@ importers: version: 0.2.17 vite: specifier: 8.2.2 - version: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + version: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) xxhash-wasm: specifier: 1.1.0 version: 1.1.0 @@ -436,7 +439,7 @@ importers: version: 7.8.2 vitest: specifier: 4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) optionalDependencies: lmdb: specifier: 3.5.6 @@ -669,7 +672,7 @@ importers: version: 1.103.1 sass-loader: specifier: 17.0.0 - version: 17.0.0(sass@1.103.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + version: 17.0.0(sass-embedded@1.103.1)(sass@1.103.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) semver: specifier: 7.8.5 version: 7.8.5 @@ -1564,6 +1567,9 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true + '@bufbuild/protobuf@2.14.0': + resolution: {integrity: sha512-C3UGsiCwSprE2NKIIFA3hCDlpXTMCAXRZuEVp88L1GY36Y41+rYL5fryE+nOFhp4p4JPQvdV8PQ4DWgHgeTE+w==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -4518,6 +4524,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colorjs.io@0.7.1: + resolution: {integrity: sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==} + colors@1.4.0: resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} engines: {node: '>=0.1.90'} @@ -7320,6 +7329,123 @@ packages: sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + sass-embedded-all-unknown@1.103.1: + resolution: {integrity: sha512-MwX2ap06TbImAqlbnH6EndgBBejk6Eq2QB6Ae2hukCn9wmXf9cRu27um3mz3tyB4oJOOku0xfHqFH/+ckYj6YA==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.103.1: + resolution: {integrity: sha512-jBXWMksyz55XeLOWvQs64BqiG5DcqflmMxcvMoA2oHtQT/7XZ02hBn502qpcF/q8Qd+qqG/hU5ccM+hGXrbQZw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.103.1: + resolution: {integrity: sha512-p36WHpsu5HEo2+NVNbI2Nmb8VgVH0xwOQyzghvZhF7ikGCzEVYI5BP28vHUZlCcgj2TUvMEJKI2lV336a+F/sg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.103.1: + resolution: {integrity: sha512-rzrH7RNntk0rx+zCE9xOiAIUt37D1cQKvFfcoX0xiePdGbNHBN8DWKaWN9vaCRfbgKCmxPjevzaqFNI/G/8E1A==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.103.1: + resolution: {integrity: sha512-EQglAJXJutuIcqgZf7YOwzao2ND6ow/OgqN+cM48/Qaeb6AimYmZiUJmM9FFcmS3Q2n1k6/2OU3l3Hlin7bFpw==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.103.1: + resolution: {integrity: sha512-rlaBeCul8pLbDRKdBAxdDxSBwFU5fbX42ci6CxW2Vbs34jWTMQ72+nE2v2KkGGce+JnYbb7UV6xDP/4uDWWi+w==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.103.1: + resolution: {integrity: sha512-VSKFfhI//AOct6ppFmAaSH7tlW2LOFECMGs77jIoWf0ZjvGch/3sc9lUTTdIaYIOnTS4qpZck+8OcI2D0G92QA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.103.1: + resolution: {integrity: sha512-rC+80/Xr9svo0ka2Zr/sjD+N1zEHTw2Urm/nsEjaVp5HSH2i22jwhTKTIqie2OLl6ti+qdYnbI0sSfxOYRtgrg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: glibc + + sass-embedded-linux-arm@1.103.1: + resolution: {integrity: sha512-tJRLPUtBwXHTnBnG7I39gQvCLreuOHLLCMyKHPR1hQ7aFNwiZjADYzKMRQPNMUqti/Os9z+6I7V13FqgQHfuzg==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: glibc + + sass-embedded-linux-musl-arm64@1.103.1: + resolution: {integrity: sha512-jMgG/C/VMo7+ShMFdk0xjJmQmYkj1PbJu/yrQiaQHpuo5pE2G+vnkEgsuM5EM3lTQ4MbMfnf715qy1fkbNPiUQ==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-arm@1.103.1: + resolution: {integrity: sha512-N8L/kgVzXpnH+8d6ErIzqvb0l8kHODR3OfPaf0Azw6DOLGtEjwDRUbHno+G7iogGzyPOjKsM8OdyqHJj/bpTdw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: musl + + sass-embedded-linux-musl-riscv64@1.103.1: + resolution: {integrity: sha512-LZm8rEvI6sKU87qPOlODsYeAs7CmWy0B4ShYkjG4OMcGCcV4Ffxn25XW4TFqmruThITQvJ8WT+WGblzdoxYgbA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-x64@1.103.1: + resolution: {integrity: sha512-rmKzgk4t6RpaDhSefbbnk5yrA4pk1nHlVhJECGay8yMfzK8eglATeY9fzPgt89xCuQk6rkOiy11e+m8tLYOW5Q==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: musl + + sass-embedded-linux-riscv64@1.103.1: + resolution: {integrity: sha512-xydQmtxla31uMrmN6z+mAogVGdEMDmkUElVq2+udomX/TPZDYJBkApEyJl237M2VLWVv29v1N4U4i2Z4xa7SDA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: glibc + + sass-embedded-linux-x64@1.103.1: + resolution: {integrity: sha512-Rln1oWm0MWKzzemOgQWrRFzfVBARwS9qxAidZPpCQHSohzPnWizFkmY1CUJad/YJNsA7sscueMdX63OEvxoM8w==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: glibc + + sass-embedded-unknown-all@1.103.1: + resolution: {integrity: sha512-p7kI4US8+n+YguKczXh+4KcodJnk+8cwv+QFdvMaz3OfPrdHV0EnhPsaVDWD9qXb1XXCEdUhpp6vQwGk3eKc3A==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.103.1: + resolution: {integrity: sha512-/0renOj3SpZGu4TA8CZJ6qZbTxGZk1khkPfiBTmV1uFB40ESYM5VsdXNj76P07bsAjIj9X2fCjTafLQFLSd9Nw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.103.1: + resolution: {integrity: sha512-fIg60j9u5YlLUU3W8FvySMR4HHIo9R12HHFQTW7njyQ8nXqgR+t+XfCzGJloQka8g583BlAfu31Q76Ljr5o+9g==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.103.1: + resolution: {integrity: sha512-UVIuFZPzz+4DXYbHzQ5gKaKkyP2IH3Sc/WoF63N2V7QIfnC2E9UpQAtiKlCVJTfBe43HRgHDBLnXpZkYAEBsNQ==} + engines: {node: '>=20.19.0'} + hasBin: true + sass-loader@17.0.0: resolution: {integrity: sha512-0Ybm8ohBQ9LcrycVrFQp/KQBNX5a3Wda9/smS0mE/xLffzEnwvV8nykOzrbiSWNzTE3IB/jiXx8O4QmDPG2+Gw==} engines: {node: '>= 22.11.0'} @@ -7661,6 +7787,14 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} + engines: {node: '>=16.0.0'} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -7974,6 +8108,9 @@ packages: resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -9240,6 +9377,8 @@ snapshots: dependencies: css-tree: 3.2.1 + '@bufbuild/protobuf@2.14.0': {} + '@colors/colors@1.5.0': {} '@conventional-changelog/git-client@3.1.2(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)': @@ -11503,9 +11642,9 @@ snapshots: lodash: 4.18.1 minimatch: 10.2.6 - '@vitejs/plugin-basic-ssl@2.3.0(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitejs/plugin-basic-ssl@2.3.0(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: - vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': dependencies: @@ -11519,7 +11658,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) '@vitest/expect@4.1.11': dependencies: @@ -11530,13 +11669,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) '@vitest/pretty-format@4.1.11': dependencies: @@ -12300,6 +12439,8 @@ snapshots: colorette@2.0.20: {} + colorjs.io@0.7.1: {} + colors@1.4.0: {} combined-stream@1.0.8: @@ -15511,9 +15652,97 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sass-loader@17.0.0(sass@1.103.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): + sass-embedded-all-unknown@1.103.1: + dependencies: + sass: 1.103.1 + optional: true + + sass-embedded-android-arm64@1.103.1: + optional: true + + sass-embedded-android-arm@1.103.1: + optional: true + + sass-embedded-android-riscv64@1.103.1: + optional: true + + sass-embedded-android-x64@1.103.1: + optional: true + + sass-embedded-darwin-arm64@1.103.1: + optional: true + + sass-embedded-darwin-x64@1.103.1: + optional: true + + sass-embedded-linux-arm64@1.103.1: + optional: true + + sass-embedded-linux-arm@1.103.1: + optional: true + + sass-embedded-linux-musl-arm64@1.103.1: + optional: true + + sass-embedded-linux-musl-arm@1.103.1: + optional: true + + sass-embedded-linux-musl-riscv64@1.103.1: + optional: true + + sass-embedded-linux-musl-x64@1.103.1: + optional: true + + sass-embedded-linux-riscv64@1.103.1: + optional: true + + sass-embedded-linux-x64@1.103.1: + optional: true + + sass-embedded-unknown-all@1.103.1: + dependencies: + sass: 1.103.1 + optional: true + + sass-embedded-win32-arm64@1.103.1: + optional: true + + sass-embedded-win32-x64@1.103.1: + optional: true + + sass-embedded@1.103.1: + dependencies: + '@bufbuild/protobuf': 2.14.0 + colorjs.io: 0.7.1 + immutable: 5.1.9 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.103.1 + sass-embedded-android-arm: 1.103.1 + sass-embedded-android-arm64: 1.103.1 + sass-embedded-android-riscv64: 1.103.1 + sass-embedded-android-x64: 1.103.1 + sass-embedded-darwin-arm64: 1.103.1 + sass-embedded-darwin-x64: 1.103.1 + sass-embedded-linux-arm: 1.103.1 + sass-embedded-linux-arm64: 1.103.1 + sass-embedded-linux-musl-arm: 1.103.1 + sass-embedded-linux-musl-arm64: 1.103.1 + sass-embedded-linux-musl-riscv64: 1.103.1 + sass-embedded-linux-musl-x64: 1.103.1 + sass-embedded-linux-riscv64: 1.103.1 + sass-embedded-linux-x64: 1.103.1 + sass-embedded-unknown-all: 1.103.1 + sass-embedded-win32-arm64: 1.103.1 + sass-embedded-win32-x64: 1.103.1 + + sass-loader@17.0.0(sass-embedded@1.103.1)(sass@1.103.1)(webpack@5.109.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): optionalDependencies: sass: 1.103.1 + sass-embedded: 1.103.1 webpack: 5.109.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) sass@1.103.1: @@ -15941,6 +16170,12 @@ snapshots: symbol-tree@3.2.4: {} + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.2.0 + + sync-message-port@1.2.0: {} + tagged-tag@1.0.0: {} tapable@2.3.3: {} @@ -16247,6 +16482,8 @@ snapshots: validator@13.15.26: {} + varint@6.0.0: {} + vary@1.1.2: {} verdaccio-audit@13.1.2(encoding@0.1.13)(supports-color@11.0.0): @@ -16325,7 +16562,7 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0): + vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -16339,14 +16576,15 @@ snapshots: jiti: 2.7.0 less: 4.9.0(supports-color@11.0.0) sass: 1.103.1 + sass-embedded: 1.103.1 terser: 5.50.0 tsx: 4.23.12 yaml: 2.9.0 - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.0.1)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -16363,7 +16601,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.0(supports-color@11.0.0))(sass-embedded@1.103.1)(sass@1.103.1)(terser@5.50.0)(tsx@4.23.12)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1