-
Notifications
You must be signed in to change notification settings - Fork 0
/
postcss.ts
365 lines (333 loc) · 10.5 KB
/
postcss.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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import * as path from "@std/path";
import type {
OnLoadArgs,
OnLoadResult,
OnResolveArgs,
OnResolveResult,
Plugin,
} from "esbuild";
import postcss from "postcss";
import type { Message, Plugin as PostCSSPlugin } from "postcss";
import postCSSModules from "postcss-modules";
/**
* Generate a scoped name for a class.
*
* @param name - The name of the class.
* @param filename - The filename of the file.
* @param css - The css of the file.
* @returns The scoped name.
*/
export type GenerateScopedNameFunction = (
name: string,
filename: string,
css: string,
) => string;
/**
* Locals convention function.
*
* @param originalClassName - The original class name.
* @param generatedClassName - The generated class name.
* @param inputFile - The input file.
* @returns The scoped name.
*/
export type LocalsConventionFunction = (
originalClassName: string,
generatedClassName: string,
inputFile: string,
) => string;
/**
* A loader for postcss modules.
*/
export class PostCSSModuleLoader {
constructor(_root: string, _plugins: PostCSSPlugin[]) {}
/**
* Fetch the class names from the file.
*
* @param file - The file.
* @param relativeTo - The relative to.
* @param depTrace - The dependency trace.
* @returns The class names.
*/
fetch(
_file: string,
_relativeTo: string,
_depTrace: string,
): Promise<{ [key: string]: string }> {
return Promise.resolve({});
}
/** The final source. */
finalSource?: string | undefined;
}
/**
* Options for postcss modules.
*/
export interface PostCSSModulesOptions {
/** Get the json from the file. */
getJSON?(
cssFilename: string,
json: { [name: string]: string },
outputFilename?: string,
): void;
/** Style of exported class names. */
localsConvention?:
| "camelCase"
| "camelCaseOnly"
| "dashes"
| "dashesOnly"
| LocalsConventionFunction;
/** Behavior of the scope. by default it is local. */
scopeBehaviour?: "global" | "local";
/** Paths to modules that should be treated as global. */
globalModulePaths?: RegExp[];
/** Style of exported class names. */
generateScopedName?: string | GenerateScopedNameFunction;
/** Prefix for the hash. */
hashPrefix?: string;
/** Whether to export globals. */
exportGlobals?: boolean;
/** The root of the module. */
root?: string;
/** The loader for the postcss modules. */
Loader?: typeof PostCSSModuleLoader;
/** Resolve the file. */
resolve?: (
file: string,
importer: string,
) => string | null | Promise<string | null>;
}
/**
* Get the files recursively from the directory.
*
* @returns an array of strings
*/
function getFilesRecursive(directory: string): string[] {
return [...Deno.readDirSync(directory)].reduce<string[]>((files, file) => {
const name = path.join(directory, file.name);
return Deno.statSync(name).isDirectory
? [...files, ...getFilesRecursive(name)]
: [...files, name];
}, []);
}
/**
* Get the dependencies from the postcss messages.
*
* @returns an array of strings
*/
function getPostCSSDependencies(messages: Message[]): string[] {
const dependencies: string[] = [];
for (const message of messages) {
if (message.type == "dir-dependency") {
dependencies.push(...getFilesRecursive(message.dir));
} else if (message.type == "dependency") {
dependencies.push(message.file);
}
}
return dependencies;
}
/**
* The results of a preprocessor.
*/
export interface PreprocessorResults {
css: string;
watchFiles?: string[];
}
/**
* A preprocessor for the PostCSS Plugin to use.
*/
export interface Preprocessor {
/** The path filter for the preprocessor. */
filter: RegExp;
/** Compile the file with the preprocessor. */
compile(path: string, fileContent: string): Promise<PreprocessorResults>;
}
/**
* Options for the postcss plugin.
*/
export interface PostCSSPluginOptions {
/**
* Array of plugins for postcss.
* If you include the postcss-module plugin in that list, set modules to false
* so that it is used instead of the default postcss-module.
*/
plugins?: PostCSSPlugin[];
/**
* Configure whether or not the postcss-modules is added to the beginning of the list of plugins.
* If this is an object, it will be used as the postcss-module's plugin options.
*
* @default true
*/
modules?: boolean | PostCSSModulesOptions;
/**
* Determines if the file should be considered a postcss module.
* By default, only files with .module in their extension are considered postcss module.
*/
isModule?: (filename: string) => boolean;
preprocessors?: Preprocessor[];
}
/**
* The postcss plugin for esbuild.
*
* ```ts
* import esbuild from "esbuild";
* import { postCSSPlugin } from "@udibo/esbuild-plugin-postcss";
*
* esbuild.build({
* plugins: [postCSSPlugin()],
* entryPoints: ["./src/index.css"],
* outdir: "./dist",
* bundle: true,
* });
* ```
*
* @param options - The options for the postcss plugin.
* @returns The postcss plugin.
*/
export const postCSSPlugin = (
options: PostCSSPluginOptions = {},
): Plugin => ({
name: "postcss",
setup(build) {
const plugins = options.plugins ?? [];
const preprocessors = options.preprocessors ?? [];
const modules = options.modules ?? true;
const {
isModule,
} = options;
const modulesMap: Map<string, Record<string, string>> = new Map();
const modulesPlugin = postCSSModules({
...(typeof modules !== "boolean" && modules ? modules : {}),
async getJSON(filepath, json, outpath) {
modulesMap.set(filepath, json);
if (
typeof modules !== "boolean" &&
typeof modules?.getJSON === "function"
) {
return modules.getJSON(filepath, json, outpath);
} else {
await Deno.writeTextFile(`${filepath}.json`, JSON.stringify(json));
}
},
});
build.onResolve(
{ filter: /\.(css|sass|scss|less|styl)$/ },
/**
* Handles resolving the path of a .css, .sass, .scss, .less, or .styl file.
* For non .css files, the file is preprocessed with the appropriate preprocessor.
* Then the postcss plugin is applied to the file.
*
* @param args - The arguments for the onResolve event.
* @returns The resolved path or null if the file is not a css file.
*/
async (
args: OnResolveArgs,
): Promise<OnResolveResult | null | undefined> => {
if (
args.namespace !== "file" && args.namespace !== "" &&
!args.namespace.startsWith("postcss-module")
) {
return null;
}
const absolutePath = path.resolve(args.resolveDir, args.path);
const ext = path.extname(absolutePath);
const sourceBaseName = path.basename(absolutePath, ext);
const module = isModule
? isModule(absolutePath)
: sourceBaseName.match(/\.module$/);
const fileContent = await Deno.readTextFile(absolutePath);
let css = ext === ".css" ? fileContent : "";
let watchFiles: string[] = [];
for (const preprocessor of preprocessors) {
if (preprocessor.filter.test(ext)) {
const results = await preprocessor.compile(
absolutePath,
fileContent,
);
css = results.css;
if (results.watchFiles) {
watchFiles = watchFiles.concat(results.watchFiles);
}
}
}
const result = await postcss(
module ? [modulesPlugin, ...plugins] : plugins,
).process(css, {
from: absolutePath,
});
if (result.opts.from) watchFiles.push(result.opts.from);
watchFiles = watchFiles.concat(getPostCSSDependencies(result.messages));
return {
namespace: module ? "postcss-module" : "postcss",
path: args.path,
watchFiles,
pluginData: {
resolveDir: args.resolveDir,
absolutePath,
kind: args.kind,
css: result.css,
},
};
},
);
build.onLoad(
{ filter: /.*/, namespace: "postcss-module" },
/**
* Handles loading of CSS modules, which contain both styles and class name mappings.
*
* For import statements (e.g., `import styles from './styles.module.css'`):
* - Returns a JS module that exports:
* - Individual class name mappings (e.g., `export const button = "button_hash"`)
* - The processed CSS as a string (`export const css = "..."`)
*
* For direct inclusion as an esbuild entrypoint:
* - Returns the processed CSS directly
*
* @param args - Contains pluginData with resolveDir, absolutePath, kind, and processed css
* @returns OnLoadResult with either JS module exports or raw CSS
*/
(args: OnLoadArgs): OnLoadResult => {
const pluginData = args.pluginData;
const absolutePath = pluginData.absolutePath as string;
const mod = modulesMap.get(absolutePath) ?? {};
const css = pluginData.css;
return {
resolveDir: pluginData.resolveDir,
loader: pluginData.kind === "import-statement" ? "js" : "css",
contents: pluginData.kind === "import-statement"
? [
...Object.entries(mod).map(([key, value]) =>
`export const ${key} = ${JSON.stringify(value)};`
),
`export const css = ${JSON.stringify(css)};`,
].join("\n")
: css,
};
},
);
build.onLoad(
{ filter: /.*/, namespace: "postcss" },
/**
* Handles loading of regular CSS files (non-modules).
*
* For import statements (e.g., `import './styles.css'`):
* - Returns a JS module that exports:
* - The processed CSS as a string (`export const css = "..."`)
*
* For direct inclusion as an esbuild entrypoint:
* - Returns the processed CSS directly
*
* @param args - Contains pluginData with resolveDir, kind, and processed css
* @returns OnLoadResult with either JS module exports or raw CSS
*/
(args: OnLoadArgs): OnLoadResult => {
const pluginData = args.pluginData;
return {
resolveDir: pluginData.resolveDir,
loader: pluginData.kind === "import-statement" ? "js" : "css",
contents: pluginData.kind === "import-statement"
? `export const css = ${JSON.stringify(pluginData.css)};`
: pluginData.css,
};
},
);
},
});