forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
/
css-plugin.ts
183 lines (163 loc) · 6.04 KB
/
css-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
* @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 createAutoPrefixerPlugin from 'autoprefixer';
import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
/**
* The lazy-loaded instance of the postcss stylesheet postprocessor.
* It is only imported and initialized if postcss is needed.
*/
let postcss: typeof import('postcss')['default'] | undefined;
/**
* An object containing the plugin options to use when processing CSS stylesheets.
*/
export interface CssPluginOptions {
/**
* Controls the use and creation of sourcemaps when processing the stylesheets.
* If true, sourcemap processing is enabled; if false, disabled.
*/
sourcemap: boolean;
/**
* Optional component data for any inline styles from Component decorator `styles` fields.
* The key is an internal angular resource URI and the value is the stylesheet content.
*/
inlineComponentData?: Record<string, string>;
/**
* The browsers to support in browserslist format when processing stylesheets.
* Some postcss plugins such as autoprefixer require the raw browserslist information instead
* of the esbuild formatted target.
*/
browsers: string[];
tailwindConfiguration?: { file: string; package: string };
}
/**
* Creates an esbuild plugin to process CSS stylesheets.
* @param options An object containing the plugin options.
* @returns An esbuild Plugin instance.
*/
export function createCssPlugin(options: CssPluginOptions): Plugin {
return {
name: 'angular-css',
async setup(build: PluginBuild): Promise<void> {
const autoprefixer = createAutoPrefixerPlugin({
overrideBrowserslist: options.browsers,
ignoreUnknownVersions: true,
});
// Autoprefixer currently does not contain a method to check if autoprefixer is required
// based on the provided list of browsers. However, it does contain a method that returns
// informational text that can be used as a replacement. The text "Awesome!" will be present
// when autoprefixer determines no actions are needed.
// ref: https://github.com/postcss/autoprefixer/blob/e2f5c26ff1f3eaca95a21873723ce1cdf6e59f0e/lib/info.js#L118
const autoprefixerInfo = autoprefixer.info({ from: build.initialOptions.absWorkingDir });
const skipAutoprefixer = autoprefixerInfo.includes('Awesome!');
if (skipAutoprefixer && !options.tailwindConfiguration) {
return;
}
postcss ??= (await import('postcss')).default;
const postcssProcessor = postcss();
if (options.tailwindConfiguration) {
const tailwind = await import(options.tailwindConfiguration.package);
postcssProcessor.use(tailwind.default({ config: options.tailwindConfiguration.file }));
}
if (!skipAutoprefixer) {
postcssProcessor.use(autoprefixer);
}
// Add a load callback to support inline Component styles
build.onLoad({ filter: /^css;/, namespace: 'angular:styles/component' }, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(
typeof data === 'string',
`component style name should always be found [${args.path}]`,
);
const [, , filePath] = args.path.split(';', 3);
return compileString(data, filePath, postcssProcessor, options);
});
// Add a load callback to support files from disk
build.onLoad({ filter: /\.css$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');
return compileString(data, args.path, postcssProcessor, options);
});
},
};
}
/**
* Compiles the provided CSS stylesheet data using a provided postcss processor and provides an
* esbuild load result that can be used directly by an esbuild Plugin.
* @param data The stylesheet content to process.
* @param filename The name of the file that contains the data.
* @param postcssProcessor A postcss processor instance to use.
* @param options The plugin options to control the processing.
* @returns An esbuild OnLoaderResult object with the processed content, warnings, and/or errors.
*/
async function compileString(
data: string,
filename: string,
postcssProcessor: import('postcss').Processor,
options: CssPluginOptions,
): Promise<OnLoadResult> {
try {
const result = await postcssProcessor.process(data, {
from: filename,
to: filename,
map: options.sourcemap && {
inline: true,
sourcesContent: true,
},
});
const rawWarnings = result.warnings();
let warnings;
if (rawWarnings.length > 0) {
const lineMappings = new Map<string, string[] | null>();
warnings = rawWarnings.map((warning) => {
const file = warning.node.source?.input.file;
if (file === undefined) {
return { text: warning.text };
}
let lines = lineMappings.get(file);
if (lines === undefined) {
lines = warning.node.source?.input.css.split(/\r?\n/);
lineMappings.set(file, lines ?? null);
}
return {
text: warning.text,
location: {
file,
line: warning.line,
column: warning.column - 1,
lineText: lines?.[warning.line - 1],
},
};
});
}
return {
contents: result.css,
loader: 'css',
warnings,
};
} catch (error) {
postcss ??= (await import('postcss')).default;
if (error instanceof postcss.CssSyntaxError) {
const lines = error.source?.split(/\r?\n/);
return {
errors: [
{
text: error.reason,
location: {
file: error.file,
line: error.line,
column: error.column && error.column - 1,
lineText: error.line === undefined ? undefined : lines?.[error.line - 1],
},
},
],
};
}
throw error;
}
}