8000 feat(@angular-devkit/build-angular): support using custom postcss con… · angular/angular-cli@7c522aa · GitHub
[go: up one dir, main page]

Skip to content

Commit 7c522aa

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular-devkit/build-angular): support using custom postcss configuration 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.
1 parent 944cbcd commit 7c522aa

File tree

6 files changed

+168
-9
lines changed

6 files changed

+168
-9
lines changed

packages/angular_devkit/build_angular/src/builders/application/options.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { I18nOptions, createI18nOptions } from '../../utils/i18n-options';
2121
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
2222
import { normalizeCacheOptions } from '../../utils/normalize-cache';
2323
import { generateEntryPoints } from '../../utils/package-chunk-sort';
24+
import { loadPostcssConfiguration } from '../../utils/postcss-configuration';
2425
import { findTailwindConfigurationFile } from '../../utils/tailwind';
2526
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
2627
import {
@@ -190,6 +191,12 @@ export async function normalizeOptions(
190191
}
191192
}
192193

194+
const postcssConfiguration = await loadPostcssConfiguration(workspaceRoot, projectRoot);
195+
// Skip tailwind configuration if postcss is customized
196+
const tailwindConfiguration = postcssConfiguration
197+
? undefined
198+
: await getTailwindConfig(workspaceRoot, projectRoot, context);
199+
193200
const globalStyles: { name: string; files: string[]; initial: boolean }[] = [];
194201
if (options.styles?.length) {
195202
const { entryPoints: stylesheetEntrypoints, noInjectNames } = normalizeGlobalStyles(
@@ -329,7 +336,8 @@ export async function normalizeOptions(
329336
serviceWorker:
330337
typeof serviceWorker === 'string' ? path.join(workspaceRoot, serviceWorker) : undefined,
331338
indexHtmlOptions,
332-
tailwindConfiguration: await getTailwindConfig(workspaceRoot, projectRoot, context),
339+
tailwindConfiguration,
340+
postcssConfiguration,
333341
i18nOptions,
334342
namedChunks,
335343
budgets: budgets?.length ? budgets : undefined,

packages/angular_devkit/build_angular/src/tools/esbuild/compiler-plugin-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function createCompilerPluginOptions(
3535
jit,
3636
cacheOptions,
3737
tailwindConfiguration,
38+
postcssConfiguration,
3839
publicPath,
3940
} = options;
4041

@@ -68,6 +69,7 @@ export function createCompilerPluginOptions(
6869
inlineStyleLanguage,
6970
preserveSymlinks,
7071
tailwindConfiguration,
72+
postcssConfiguration,
7173
cacheOptions,
7274
publicPath,
7375
},

packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export function createGlobalStylesBundleOptions(
2727
externalDependencies,
2828
stylePreprocessorOptions,
2929
tailwindConfiguration,
30+
postcssConfiguration,
3031
cacheOptions,
3132
publicPath,
3233
} = options;
@@ -64,6 +65,7 @@ export function createGlobalStylesBundleOptions(
6465
},
6566
includePaths: stylePreprocessorOptions?.includePaths,
6667
tailwindConfiguration,
68+
postcssConfiguration,
6769
cacheOptions,
6870
publicPath,
6971
},

packages/angular_devkit/build_angular/src/tools/esbuild/stylesheets/bundle-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type { BuildOptions, Plugin } from 'esbuild';
1010
import path from 'node:path';
1111
import { NormalizedCachedOptions } from '../../../utils/normalize-cache';
12+
import { PostcssConfiguration } from '../../../utils/postcss-configuration';
1213
import { LoadResultCache } from '../load-result-cache';
1314
import { createCssInlineFontsPlugin } from './css-inline-fonts-plugin';
1415
import { CssStylesheetLanguage } from './css-language';
@@ -28,6 +29,7 @@ export interface BundleStylesheetOptions {
2829
externalDependencies?: string[];
2930
target: string[];
3031
tailwindConfiguration?: { file: string; package: string };
32+
postcssConfiguration?: PostcssConfiguration;
3133
publicPath?: string;
3234
cacheOptions: NormalizedCachedOptions;
3335
}
@@ -48,6 +50,7 @@ export function createStylesheetBundleOptions(
4850
includePaths,
4951
inlineComponentData,
5052
tailwindConfiguration: options.tailwindConfiguration,
53+
postcssConfiguration: options.postcssConfiguration,
5154
},
5255
cache,
5356
);

packages/angular_devkit/build_angular/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import glob from 'fast-glob';
1111
import assert from 'node:assert';
1212
import { readFile } from 'node:fs/promises';
1313
import { extname } from 'node:path';
14+
import type { PostcssConfiguration } from '../../../utils/postcss-configuration';
1415
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
1516

1617
/**
@@ -47,6 +48,13 @@ export interface StylesheetPluginOptions {
4748
* by the configuration file.
4849
*/
4950
tailwindConfiguration?: { file: string; package: string };
51+
52+
/**
53+
* Optional configuration object for custom postcss usage. If present, postcss will be
54+
* initialized and used for every stylesheet. This overrides the tailwind integration
55+
* and any tailwind usage must be manually configured in the custom postcss usage.
56+
*/
57+
postcssConfiguration?: PostcssConfiguration;
5058
}
5159

5260
/**< 10000 /div>
@@ -92,7 +100,11 @@ export class StylesheetPluginFactory {
92100

93101
create(language: Readonly<StylesheetLanguage>): Plugin {
94102
// Return a noop plugin if no load actions are required
95-
if (!language.process && !this.options.tailwindConfiguration) {
103+
if (
104+
!language.process &&
105+
!this.options.postcssConfiguration &&
106+
!this.options.tailwindConfiguration
107+
) {
96108
return {
97109
name: 'angular-' + language.name,
98110
setup() {},
@@ -106,7 +118,26 @@ export class StylesheetPluginFactory {
106118
return this.postcssProcessor;
107119
}
108120

109-
if (options.tailwindConfiguration) {
121+
if (options.postcssConfiguration) {
122+
const postCssInstanceKey = JSON.stringify(options.postcssConfiguration);
123+
124+
this.postcssProcessor = postcssProcessor.get(postCssInstanceKey)?.deref();
125+
126+
if (!this.postcssProcessor) {
127+
postcss ??= (await import('postcss')).default;
128+
this.postcssProcessor = postcss();
129+
130+
for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) {
131+
const { default: plugin } = await import(pluginName);
132+
if (typeof plugin !== 'function' || plugin.postcss !== true) {
133+
throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`);
134+
}
135+
this.postcssProcessor.use(plugin(pluginOptions));
136+
}
137+
138+
postcssProcessor.set(postCssInstanceKey, new WeakRef(this.postcssProcessor));
139+
}
140+
} else if (options.tailwindConfiguration) {
110141
const { package: tailwindPackage, file: config } = options.tailwindConfiguration;
111142
const postCssInstanceKey = tailwindPackage + ':' + config;
112143
this.postcssProcessor = postcssProcessor.get(postCssInstanceKey)?.deref();
@@ -196,15 +227,13 @@ async function processStylesheet(
196227
};
197228
}
198229

199-
// Return early if there are no contents to further process
200-
if (!result.contents) {
230+
// Return early if there are no contents to further process or there are errors
231+
if (!result.contents || result.errors?.length) {
201232
return result;
202233
}
203234

204-
// Only use postcss if Tailwind processing is required.
205-
// NOTE: If postcss is used for more than just Tailwind in the future this check MUST
206-
// be updated to account for the additional use.
207-
if (postcssProcessor && !result.errors?.length && hasTailwindKeywords(result.contents)) {
235+
// Only use postcss if Tailwind processing is required or custom postcss is present.
236+
if (postcssProcessor && (options.postcssConfiguration || hasTailwindKeywords(result.contents))) {
208237
const postcssResult = await compileString(
209238
typeof result.contents === 'string'
210239
? result.contents
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { readFile, readdir } from 'node:fs/promises';
10+
import { join } from 'node:path';
11+
12+
export interface PostcssConfiguration {
13+
plugins: [name: string, options?: object][];
14+
}
15+
16+
interface RawPostcssConfiguration {
17+
plugins?: Record<string, object | boolean> | (string | [string, object])[];
18+
}
19+
20+
const postcssConfigurationFiles: string[] = ['postcss.config.json', '.postcssrc.json'];
21+
22+
interface SearchDirectory {
23+
root: string;
24+
files: Set<string>;
25+
}
26+
27+
async function generateSearchDirectories(roots: string[]): Promise<SearchDirectory[]> {
28+
return await Promise.all(
29+
roots.map((root) =>
30+
readdir(root, { withFileTypes: true }).then((entries) => ({
31+
root,
32+
files: new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name)),
33+
})),
34+
),
35+
);
36+
}
37+
38+
function findFile(
39+
searchDirectories: SearchDirectory[],
40+
potentialFiles: string[],
41+
): string | undefined {
42+
for (const { root, files } of searchDirectories) {
43+
for (const potential of potentialFiles) {
44+
if (files.has(potential)) {
45+
return join(root, potential);
46+
}
47+
}
48+
}
49+
50+
return undefined;
51+
}
52+
53+
async function readPostcssConfiguration(
54+
configurationFile: string,
55+
): Promise<RawPostcssConfiguration> {
56+
const data = await readFile(configurationFile, 'utf-8');
57+
const config = JSON.parse(data) as RawPostcssConfiguration;
58+
59+
return config;
60+
}
61+
62+
export async function loadPostcssConfiguration(
63+
workspaceRoot: string,
64+
projectRoot: string,
65+
): Promise<PostcssConfiguration | undefined> {
66+
// A configuration file can exist in the project or workspace root
67+
const searchDirectories = await generateSearchDirectories([projectRoot, workspaceRoot]);
68+
69+
const configPath = findFile(searchDirectories, postcssConfigurationFiles);
70+
if (!configPath) {
71+
return undefined;
72+
}
73+
74+
const raw = await readPostcssConfiguration(configPath);
75+
76+
// If no plugins are defined, consider it equivalent to no configuration
77+
if (!raw.plugins || typeof raw.plugins !== 'object') {
78+
return undefined;
79+
}
80+
81+
// Normalize plugin array form
82+
if (Array.isArray(raw.plugins)) {
83+
if (raw.plugins.length < 1) {
84+
return undefined;
85+
}
86+
87+
const config: PostcssConfiguration = { plugins: [] };
88+
for (const element of raw.plugins) {
89+
if (typeof element === 'string') {
90+
config.plugins.push([element]);
91+
} else {
92+
config.plugins.push(element);
93+
}
94+
}
95+
96+
return config;
97+
}
98+
99+
// Normalize plugin object map form
100+
const entries = Object.entries(raw.plugins);
101+
if (entries.length < 1) {
102+
return undefined;
103+
}
104+
105+
const config: PostcssConfiguration = { plugins: [] };
106+
for (const [name, options] of entries) {
107+
if (!options || typeof options !== 'object') {
108+
continue;
109+
}
110+
111+
config.plugins.push([name, options]);
112+
}
113+
114+
return config;
115+
}

0 commit comments

Comments
 (0)
0