Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support using custom postcss con…
Browse files Browse the repository at this point in the history
…figuration with application builder

When using the `application` builder, the usage of a custom postcss configuration is now supported.
The builder will automatically detect and use specific postcss configuration files if present in
either the project root directory or the workspace root. Files present in the project root will have
priority over a workspace root file. If using a custom postcss configuration file, the automatic
tailwind integration will be disabled. To use both a custom postcss configuration and tailwind, the
tailwind setup must be included in the custom postcss configuration file.

The configuration files must be JSON and named one of the following:
* `postcss.config.json`
* `.postcssrc.json`

A configuration file can use either an array form or an object form to setup plugins.
An example of the array form:
```
{
    "plugins": [
        "tailwindcss",
        ["rtlcss", { "useCalc": true }]
    ]
}
```

The same in an object form:
```
{
    "plugins": {
        "tailwindcss": {},
        "rtlcss": { "useCalc": true }
    }
}
```

NOTE: Using a custom postcss configuration may result in reduced build and rebuild
performance. Postcss will be used to process all global and component stylesheets
when a custom configuration is present. Without a custom postcss configuration,
postcss is only used for a stylesheet when tailwind is enabled and the stylesheet
requires tailwind processing.
  • Loading branch information
clydin authored and alan-agius4 committed Jan 31, 2024
1 parent 944cbcd commit 7c522aa
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { I18nOptions, createI18nOptions } from '../../utils/i18n-options';
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
import { normalizeCacheOptions } from '../../utils/normalize-cache';
import { generateEntryPoints } from '../../utils/package-chunk-sort';
import { loadPostcssConfiguration } from '../../utils/postcss-configuration';
import { findTailwindConfigurationFile } from '../../utils/tailwind';
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
import {
Expand Down Expand Up @@ -190,6 +191,12 @@ export async function normalizeOptions(
}
}

const postcssConfiguration = await loadPostcssConfiguration(workspaceRoot, projectRoot);
// Skip tailwind configuration if postcss is customized
const tailwindConfiguration = postcssConfiguration
? undefined
: await getTailwindConfig(workspaceRoot, projectRoot, context);

const globalStyles: { name: string; files: string[]; initial: boolean }[] = [];
if (options.styles?.length) {
const { entryPoints: stylesheetEntrypoints, noInjectNames } = normalizeGlobalStyles(
Expand Down Expand Up @@ -329,7 +336,8 @@ export async function normalizeOptions(
serviceWorker:
typeof serviceWorker === 'string' ? path.join(workspaceRoot, serviceWorker) : undefined,
indexHtmlOptions,
tailwindConfiguration: await getTailwindConfig(workspaceRoot, projectRoot, context),
tailwindConfiguration,
postcssConfiguration,
i18nOptions,
namedChunks,
budgets: budgets?.length ? budgets : undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function createCompilerPluginOptions(
jit,
cacheOptions,
tailwindConfiguration,
postcssConfiguration,
publicPath,
} = options;

Expand Down Expand Up @@ -68,6 +69,7 @@ export function createCompilerPluginOptions(
inlineStyleLanguage,
preserveSymlinks,
tailwindConfiguration,
postcssConfiguration,
cacheOptions,
publicPath,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function createGlobalStylesBundleOptions(
externalDependencies,
stylePreprocessorOptions,
tailwindConfiguration,
postcssConfiguration,
cacheOptions,
publicPath,
} = options;
Expand Down Expand Up @@ -64,6 +65,7 @@ export function createGlobalStylesBundleOptions(
},
includePaths: stylePreprocessorOptions?.includePaths,
tailwindConfiguration,
postcssConfiguration,
cacheOptions,
publicPath,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { BuildOptions, Plugin } from 'esbuild';
import path from 'node:path';
import { NormalizedCachedOptions } from '../../../utils/normalize-cache';
import { PostcssConfiguration } from '../../../utils/postcss-configuration';
import { LoadResultCache } from '../load-result-cache';
import { createCssInlineFontsPlugin } from './css-inline-fonts-plugin';
import { CssStylesheetLanguage } from './css-language';
Expand All @@ -28,6 +29,7 @@ export interface BundleStylesheetOptions {
externalDependencies?: string[];
target: string[];
tailwindConfiguration?: { file: string; package: string };
postcssConfiguration?: PostcssConfiguration;
publicPath?: string;
cacheOptions: NormalizedCachedOptions;
}
Expand All @@ -48,6 +50,7 @@ export function createStylesheetBundleOptions(
includePaths,
inlineComponentData,
tailwindConfiguration: options.tailwindConfiguration,
postcssConfiguration: options.postcssConfiguration,
},
cache,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import glob from 'fast-glob';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { extname } from 'node:path';
import type { PostcssConfiguration } from '../../../utils/postcss-configuration';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';

/**
Expand Down Expand Up @@ -47,6 +48,13 @@ export interface StylesheetPluginOptions {
* by the configuration file.
*/
tailwindConfiguration?: { file: string; package: string };

/**
* Optional configuration object for custom postcss usage. If present, postcss will be
* initialized and used for every stylesheet. This overrides the tailwind integration
* and any tailwind usage must be manually configured in the custom postcss usage.
*/
postcssConfiguration?: PostcssConfiguration;
}

/**
Expand Down Expand Up @@ -92,7 +100,11 @@ export class StylesheetPluginFactory {

create(language: Readonly<StylesheetLanguage>): Plugin {
// Return a noop plugin if no load actions are required
if (!language.process && !this.options.tailwindConfiguration) {
if (
!language.process &&
!this.options.postcssConfiguration &&
!this.options.tailwindConfiguration
) {
return {
name: 'angular-' + language.name,
setup() {},
Expand All @@ -106,7 +118,26 @@ export class StylesheetPluginFactory {
return this.postcssProcessor;
}

if (options.tailwindConfiguration) {
if (options.postcssConfiguration) {
const postCssInstanceKey = JSON.stringify(options.postcssConfiguration);

this.postcssProcessor = postcssProcessor.get(postCssInstanceKey)?.deref();

if (!this.postcssProcessor) {
postcss ??= (await import('postcss')).default;
this.postcssProcessor = postcss();

for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) {
const { default: plugin } = await import(pluginName);
if (typeof plugin !== 'function' || plugin.postcss !== true) {
throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`);
}
this.postcssProcessor.use(plugin(pluginOptions));
}

postcssProcessor.set(postCssInstanceKey, new WeakRef(this.postcssProcessor));
}
} else if (options.tailwindConfiguration) {
const { package: tailwindPackage, file: config } = options.tailwindConfiguration;
const postCssInstanceKey = tailwindPackage + ':' + config;
this.postcssProcessor = postcssProcessor.get(postCssInstanceKey)?.deref();
Expand Down Expand Up @@ -196,15 +227,13 @@ async function processStylesheet(
};
}

// Return early if there are no contents to further process
if (!result.contents) {
// Return early if there are no contents to further process or there are errors
if (!result.contents || result.errors?.length) {
return result;
}

// Only use postcss if Tailwind processing is required.
// NOTE: If postcss is used for more than just Tailwind in the future this check MUST
// be updated to account for the additional use.
if (postcssProcessor && !result.errors?.length && hasTailwindKeywords(result.contents)) {
// Only use postcss if Tailwind processing is required or custom postcss is present.
if (postcssProcessor && (options.postcssConfiguration || hasTailwindKeywords(result.contents))) {
const postcssResult = await compileString(
typeof result.contents === 'string'
? result.contents
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @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 { readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';

export interface PostcssConfiguration {
plugins: [name: string, options?: object][];
}

interface RawPostcssConfiguration {
plugins?: Record<string, object | boolean> | (string | [string, object])[];
}

const postcssConfigurationFiles: string[] = ['postcss.config.json', '.postcssrc.json'];

interface SearchDirectory {
root: string;
files: Set<string>;
}

async function generateSearchDirectories(roots: string[]): Promise<SearchDirectory[]> {
return await Promise.all(
roots.map((root) =>
readdir(root, { withFileTypes: true }).then((entries) => ({
root,
files: new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name)),
})),
),
);
}

function findFile(
searchDirectories: SearchDirectory[],
potentialFiles: string[],
): string | undefined {
for (const { root, files } of searchDirectories) {
for (const potential of potentialFiles) {
if (files.has(potential)) {
return join(root, potential);
}
}
}

return undefined;
}

async function readPostcssConfiguration(
configurationFile: string,
): Promise<RawPostcssConfiguration> {
const data = await readFile(configurationFile, 'utf-8');
const config = JSON.parse(data) as RawPostcssConfiguration;

return config;
}

export async function loadPostcssConfiguration(
workspaceRoot: string,
projectRoot: string,
): Promise<PostcssConfiguration | undefined> {
// A configuration file can exist in the project or workspace root
const searchDirectories = await generateSearchDirectories([projectRoot, workspaceRoot]);

const configPath = findFile(searchDirectories, postcssConfigurationFiles);
if (!configPath) {
return undefined;
}

const raw = await readPostcssConfiguration(configPath);

// If no plugins are defined, consider it equivalent to no configuration
if (!raw.plugins || typeof raw.plugins !== 'object') {
return undefined;
}

// Normalize plugin array form
if (Array.isArray(raw.plugins)) {
if (raw.plugins.length < 1) {
return undefined;
}

const config: PostcssConfiguration = { plugins: [] };
for (const element of raw.plugins) {
if (typeof element === 'string') {
config.plugins.push([element]);
} else {
config.plugins.push(element);
}
}

return config;
}

// Normalize plugin object map form
const entries = Object.entries(raw.plugins);
if (entries.length < 1) {
return undefined;
}

const config: PostcssConfiguration = { plugins: [] };
for (const [name, options] of entries) {
if (!options || typeof options !== 'object') {
continue;
}

config.plugins.push([name, options]);
}

return config;
}

0 comments on commit 7c522aa

Please sign in to comment.