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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ import {
profileSync,
resetCumulativeDurations,
} from './profiling';
import { BundleStylesheetOptions, bundleStylesheetFile, bundleStylesheetText } from './stylesheets';
import { BundleStylesheetOptions, bundleComponentStylesheet } from './stylesheets';

/**
* A counter for component styles used to generate unique build-time identifiers for each stylesheet.
*/
let componentStyleCounter = 0;

/**
* Converts TypeScript Diagnostic related information into an esbuild compatible note object.
Expand Down Expand Up @@ -150,7 +155,7 @@ export interface CompilerPluginOptions {
// eslint-disable-next-line max-lines-per-function
export function createCompilerPlugin(
pluginOptions: CompilerPluginOptions,
styleOptions: BundleStylesheetOptions,
styleOptions: BundleStylesheetOptions & { inlineStyleLanguage: string },
): Plugin {
return {
name: 'angular-compiler',
Expand Down Expand Up @@ -253,21 +258,15 @@ export function createCompilerPlugin(
// Stylesheet file only exists for external stylesheets
const filename = stylesheetFile ?? containingFile;

// Temporary workaround for lack of virtual file support in the Sass plugin.
// External Sass stylesheets are transformed using the file instead of the already read content.
let stylesheetResult;
if (filename.endsWith('.scss') || filename.endsWith('.sass')) {
stylesheetResult = await bundleStylesheetFile(filename, styleOptions);
} else {
stylesheetResult = await bundleStylesheetText(
data,
{
resolvePath: path.dirname(filename),
virtualName: filename,
},
styleOptions,
);
}
const stylesheetResult = await bundleComponentStylesheet(
// TODO: Evaluate usage of a fast hash instead
`${++componentStyleCounter}`,
styleOptions.inlineStyleLanguage,
data,
filename,
!stylesheetFile,
styleOptions,
);

const { contents, resourceFiles, errors, warnings } = stylesheetResult;
(result.errors ??= []).push(...errors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ const UNSUPPORTED_OPTIONS: Array<keyof BrowserBuilderOptions> = [
// 'i18nDuplicateTranslation',
// 'i18nMissingTranslation',

// * Stylesheet preprocessor support
'inlineStyleLanguage',
// The following option has no effect until preprocessors are supported
// 'stylePreprocessorOptions',

// * Deprecated
'deployUrl',

Expand Down Expand Up @@ -60,12 +55,13 @@ export function logExperimentalWarnings(options: BrowserBuilderOptions, context:
if (typeof value === 'object' && Object.keys(value).length === 0) {
continue;
}
if (unsupportedOption === 'inlineStyleLanguage' && value === 'css') {
continue;
}

context.logger.warn(
`The '${unsupportedOption}' option is currently unsupported by this experimental builder and will be ignored.`,
);
}

if (options.inlineStyleLanguage === 'less') {
context.logger.warn('The less stylesheet preprocessor is not currently supported.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ function createCodeBundleOptions(
preserveSymlinks,
stylePreprocessorOptions,
advancedOptimizations,
inlineStyleLanguage,
} = options;

return {
Expand Down Expand Up @@ -292,6 +293,7 @@ function createCodeBundleOptions(
includePaths: stylePreprocessorOptions?.includePaths,
externalDependencies,
target,
inlineStyleLanguage,
},
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export async function normalizeOptions(
buildOptimizer,
crossOrigin,
externalDependencies,
inlineStyleLanguage = 'css',
poll,
preserveSymlinks,
stylePreprocessorOptions,
Expand All @@ -151,6 +152,7 @@ export async function normalizeOptions(
cacheOptions,
crossOrigin,
externalDependencies,
inlineStyleLanguage,
poll,
// If not explicitly set, default to the Node.js process argument
preserveSymlinks: preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@
* found in the LICENSE file at https://angular.io/license
*/

import type { PartialMessage, Plugin, PluginBuild } from 'esbuild';
import type { OnLoadResult, PartialMessage, Plugin, PluginBuild, ResolveResult } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import { dirname, extname, join, relative } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { CompileResult, Exception } from 'sass';
import type { CompileResult, Exception, Syntax } from 'sass';
import {
FileImporterWithRequestContextOptions,
SassWorkerImplementation,
} from '../../sass/sass-service';

export interface SassPluginOptions {
sourcemap: boolean;
loadPaths?: string[];
inlineComponentData?: Record<string, string>;
}

let sassWorkerPool: SassWorkerImplementation | undefined;

function isSassException(error: unknown): error is Exception {
Expand All @@ -27,7 +34,7 @@ export function shutdownSassWorkerPool(): void {
sassWorkerPool = undefined;
}

export function createSassPlugin(options: { sourcemap: boolean; loadPaths?: string[] }): Plugin {
export function createSassPlugin(options: SassPluginOptions): Plugin {
return {
name: 'angular-sass',
setup(build: PluginBuild): void {
Expand Down Expand Up @@ -55,105 +62,123 @@ export function createSassPlugin(options: { sourcemap: boolean; loadPaths?: stri
return result;
};

build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
// Lazily load Sass when a Sass file is found
sassWorkerPool ??= new SassWorkerImplementation(true);

const warnings: PartialMessage[] = [];
try {
const data = await readFile(args.path, 'utf-8');
const { css, sourceMap, loadedUrls } = await sassWorkerPool.compileStringAsync(data, {
url: pathToFileURL(args.path),
style: 'expanded',
loadPaths: options.loadPaths,
sourceMap: options.sourcemap,
sourceMapIncludeSources: options.sourcemap,
quietDeps: true,
importers: [
{
findFileUrl: async (
url,
{ previousResolvedModules }: FileImporterWithRequestContextOptions,
): Promise<URL | null> => {
const result = await resolveUrl(url, previousResolvedModules);

// Check for package deep imports
if (!result.path) {
const parts = url.split('/');
const hasScope = parts.length >= 2 && parts[0].startsWith('@');
const [nameOrScope, nameOrFirstPath, ...pathPart] = parts;
const packageName = hasScope
? `${nameOrScope}/${nameOrFirstPath}`
: nameOrScope;

const packageResult = await resolveUrl(
packageName + '/package.json',
previousResolvedModules,
);

if (packageResult.path) {
return pathToFileURL(
join(
dirname(packageResult.path),
!hasScope ? nameOrFirstPath : '',
...pathPart,
),
);
}
}

return result.path ? pathToFileURL(result.path) : null;
},
},
],
logger: {
warn: (text, { deprecation, span }) => {
warnings.push({
text: deprecation ? 'Deprecation' : text,
location: span && {
file: span.url && fileURLToPath(span.url),
lineText: span.context,
// Sass line numbers are 0-based while esbuild's are 1-based
line: span.start.line + 1,
column: span.start.column,
},
notes: deprecation ? [{ text }] : undefined,
});
},
},
});
build.onLoad(
{ filter: /^angular:styles\/component;s[ac]ss;/, namespace: 'angular:styles/component' },
async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(data, `component style name should always be found [${args.path}]`);

return {
loader: 'css',
contents: sourceMap
? `${css}\n${sourceMapToUrlComment(sourceMap, dirname(args.path))}`
: css,
watchFiles: loadedUrls.map((url) => fileURLToPath(url)),
warnings,
};
} catch (error) {
if (isSassException(error)) {
const file = error.span.url ? fileURLToPath(error.span.url) : undefined;

return {
loader: 'css',
errors: [
{
text: error.message,
},
],
warnings,
watchFiles: file ? [file] : undefined,
};
}
const [, language, , filePath] = args.path.split(';', 4);
const syntax = language === 'sass' ? 'indented' : 'scss';

throw error;
}
return compileString(data, filePath, syntax, options, resolveUrl);
},
);

build.onLoad({ filter: /\.s[ac]ss$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');
const syntax = extname(args.path).toLowerCase() === '.sass' ? 'indented' : 'scss';

return compileString(data, args.path, syntax, options, resolveUrl);
});
},
};
}

async function compileString(
data: string,
filePath: string,
syntax: Syntax,
options: SassPluginOptions,
resolveUrl: (url: string, previousResolvedModules?: Set<string>) => Promise<ResolveResult>,
): Promise<OnLoadResult> {
// Lazily load Sass when a Sass file is found
sassWorkerPool ??= new SassWorkerImplementation(true);

const warnings: PartialMessage[] = [];
try {
const { css, sourceMap, loadedUrls } = await sassWorkerPool.compileStringAsync(data, {
url: pathToFileURL(filePath),
style: 'expanded',
syntax,
loadPaths: options.loadPaths,
sourceMap: options.sourcemap,
sourceMapIncludeSources: options.sourcemap,
quietDeps: true,
importers: [
{
findFileUrl: async (
url,
{ previousResolvedModules }: FileImporterWithRequestContextOptions,
): Promise<URL | null> => {
const result = await resolveUrl(url, previousResolvedModules);

// Check for package deep imports
if (!result.path) {
const parts = url.split('/');
const hasScope = parts.length >= 2 && parts[0].startsWith('@');
const [nameOrScope, nameOrFirstPath, ...pathPart] = parts;
const packageName = hasScope ? `${nameOrScope}/${nameOrFirstPath}` : nameOrScope;

const packageResult = await resolveUrl(
packageName + '/package.json',
previousResolvedModules,
);

if (packageResult.path) {
return pathToFileURL(
join(dirname(packageResult.path), !hasScope ? nameOrFirstPath : '', ...pathPart),
);
}
}

return result.path ? pathToFileURL(result.path) : null;
},
},
],
logger: {
warn: (text, { deprecation, span }) => {
warnings.push({
text: deprecation ? 'Deprecation' : text,
location: span && {
file: span.url && fileURLToPath(span.url),
lineText: span.context,
// Sass line numbers are 0-based while esbuild's are 1-based
line: span.start.line + 1,
column: span.start.column,
},
notes: deprecation ? [{ text }] : undefined,
});
},
},
});

return {
loader: 'css',
contents: sourceMap ? `${css}\n${sourceMapToUrlComment(sourceMap, dirname(filePath))}` : css,
watchFiles: loadedUrls.map((url) => fileURLToPath(url)),
warnings,
};
} catch (error) {
if (isSassException(error)) {
const file = error.span.url ? fileURLToPath(error.span.url) : undefined;

return {
loader: 'css',
errors: [
{
text: error.message,
},
],
warnings,
watchFiles: file ? [file] : undefined,
};
}

throw error;
}
}

function sourceMapToUrlComment(
sourceMap: Exclude<CompileResult['sourceMap'], undefined>,
root: string,
Expand Down
Loading