Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): add Less stylesheet support to e…
Browse files Browse the repository at this point in the history
…xperimental esbuild-based builder

When using the experimental esbuild-based browser application builder, stylesheets written in the Less
stylesheet language can now be used throughout an application. The support allows Less stylesheets to
be used in all locations where CSS and/or Sass can be used. This includes global stylesheets and both
inline and external component styles. When using inline component styles, the `inlineLanguageStyle`
build option must be set to `less`.
Currently, import resolution within a Less stylesheet is limited to default Less behavior which does not
include full node package resolution. Full resolution behavior will be added in a future change.
  • Loading branch information
clydin authored and angular-robot[bot] committed Feb 8, 2023
1 parent c07fbf6 commit 01b3bcf
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"@types/inquirer": "^8.0.0",
"@types/jasmine": "~4.3.0",
"@types/karma": "^6.3.0",
"@types/less": "^3.0.3",
"@types/loader-utils": "^2.0.0",
"@types/minimatch": "5.1.2",
"@types/node": "^14.15.0",
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/build_angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ ts_library(
"@npm//@types/glob",
"@npm//@types/inquirer",
"@npm//@types/karma",
"@npm//@types/less",
"@npm//@types/loader-utils",
"@npm//@types/node",
"@npm//@types/parse5-html-rewriting-stream",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,4 @@ export function logExperimentalWarnings(options: BrowserBuilderOptions, context:
`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
@@ -0,0 +1,105 @@
/**
* @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.io/license
*/

import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';

/**
* The lazy-loaded instance of the less stylesheet preprocessor.
* It is only imported and initialized if a less stylesheet is used.
*/
let lessPreprocessor: typeof import('less') | undefined;

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

interface LessException extends Error {
filename: string;
line: number;
column: number;
extract?: string[];
}

function isLessException(error: unknown): error is LessException {
return !!error && typeof error === 'object' && 'column' in error;
}

export function createLessPlugin(options: LessPluginOptions): Plugin {
return {
name: 'angular-less',
setup(build: PluginBuild): void {
// Add a load callback to support inline Component styles
build.onLoad({ filter: /^less;/, namespace: 'angular:styles/component' }, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(data, `component style name should always be found [${args.path}]`);

const [, , filePath] = args.path.split(';', 3);

return compileString(data, filePath, options);
});

// Add a load callback to support files from disk
build.onLoad({ filter: /\.less$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');

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

async function compileString(
data: string,
filename: string,
options: LessPluginOptions,
): Promise<OnLoadResult> {
const less = (lessPreprocessor ??= (await import('less')).default);

try {
const result = await less.render(data, {
filename,
paths: options.includePaths,
rewriteUrls: 'all',
sourceMap: options.sourcemap
? {
sourceMapFileInline: true,
outputSourceFiles: true,
}
: undefined,
} as Less.Options);

return {
contents: result.css,
loader: 'css',
};
} catch (error) {
if (isLessException(error)) {
return {
errors: [
{
text: error.message,
location: {
file: error.filename,
line: error.line,
column: error.column,
// Middle element represents the line containing the error
lineText: error.extract && error.extract[Math.trunc(error.extract.length / 2)],
},
},
],
loader: 'css',
};
}

throw error;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { BuildOptions, OutputFile } from 'esbuild';
import * as path from 'node:path';
import { createCssResourcePlugin } from './css-resource-plugin';
import { BundlerContext } from './esbuild';
import { createLessPlugin } from './less-plugin';
import { createSassPlugin } from './sass-plugin';

/**
Expand All @@ -32,6 +33,11 @@ export function createStylesheetBundleOptions(
options: BundleStylesheetOptions,
inlineComponentData?: Record<string, string>,
): BuildOptions & { plugins: NonNullable<BuildOptions['plugins']> } {
// Ensure preprocessor include paths are absolute based on the workspace root
const includePaths = options.includePaths?.map((includePath) =>
path.resolve(options.workspaceRoot, includePath),
);

return {
absWorkingDir: options.workspaceRoot,
bundle: true,
Expand All @@ -52,10 +58,12 @@ export function createStylesheetBundleOptions(
plugins: [
createSassPlugin({
sourcemap: !!options.sourcemap,
// Ensure Sass load paths are absolute based on the workspace root
loadPaths: options.includePaths?.map((includePath) =>
path.resolve(options.workspaceRoot, includePath),
),
loadPaths: includePaths,
inlineComponentData,
}),
createLessPlugin({
sourcemap: !!options.sourcemap,
includePaths,
inlineComponentData,
}),
createCssResourcePlugin(),
Expand Down
8 changes: 5 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@

"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#ffd5dec0bf78a2c8ff068482ad3c8434c21b54c7":
version "0.0.0-fb077c1937f280aac6327969fa3ab50f98b4d25a"
uid ffd5dec0bf78a2c8ff068482ad3c8434c21b54c7
resolved "https://github.com/angular/dev-infra-private-build-tooling-builds.git#ffd5dec0bf78a2c8ff068482ad3c8434c21b54c7"
dependencies:
"@angular-devkit/build-angular" "15.2.0-next.3"
Expand Down Expand Up @@ -307,7 +306,6 @@

"@angular/ng-dev@https://github.com/angular/dev-infra-private-ng-dev-builds.git#f4601b680d6d0017880115cc8ee99249c34f0c12":
version "0.0.0-fb077c1937f280aac6327969fa3ab50f98b4d25a"
uid f4601b680d6d0017880115cc8ee99249c34f0c12
resolved "https://github.com/angular/dev-infra-private-ng-dev-builds.git#f4601b680d6d0017880115cc8ee99249c34f0c12"
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
Expand Down Expand Up @@ -3170,6 +3168,11 @@
"@types/node" "*"
log4js "^6.4.1"

"@types/less@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.3.tgz#f9451dbb9548d25391107d65d6401a0cfb15db92"
integrity sha512-1YXyYH83h6We1djyoUEqTlVyQtCfJAFXELSKW2ZRtjHD4hQ82CC4lvrv5D0l0FLcKBaiPbXyi3MpMsI9ZRgKsw==

"@types/loader-utils@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-2.0.3.tgz#fbc2337358f8f4a7dc532ac0a3646c74275edf2d"
Expand Down Expand Up @@ -10107,7 +10110,6 @@ [email protected]:

"sauce-connect-proxy@https://saucelabs.com/downloads/sc-4.8.1-linux.tar.gz":
version "0.0.0"
uid "9c16682e4c9716734432789884f868212f95f563"
resolved "https://saucelabs.com/downloads/sc-4.8.1-linux.tar.gz#9c16682e4c9716734432789884f868212f95f563"

saucelabs@^1.5.0:
Expand Down

0 comments on commit 01b3bcf

Please sign in to comment.